Java学习笔记之----------多线程
-
- 进程
- 线程
- 多线程存在的意义
- 创建多线程
- 线程的几种状态
- 创建多线程(推荐)
- 多线程的安全问题
进程
进程:应用程序在内存中分配的空间(正在运行中的程序)
线程
线程:
进程中负责程序执行的执行单元,也称为执行路径
一个进程至少有一个线程负责该进程的执行,称主线程
如果一个进程启用了多个线程,就称该程序为多线程程序
多线程存在的意义
多线程有时并不能提高效率,所以多线程存在的意义不是提高效率,多线程其实是合理使用资源
解决多部分代码同时执行的需求,合理使用CPU资源
多线程的运行是根据CPU的切换完成的,如何切换是由CPU决定的,所以多线程运行具有随机性
JVM中的线程至少有两个
一个是负责自定义代码运行的
一个是负责垃圾回收的
class Demo{
//定义垃圾回收方法public void finalize(){
System.out.println("clear is ok");}
}
public class GcTest{
public static void main(String args[]){
new Demo();new Demo();new Demo();System.gc();//启动垃圾回收器System.out.println("TEST OVER!"); }
}
可以看到,垃圾回收器启动了之后不一定执行,执行与否不确定,由CPU说了算
注:因为我的JDK版本较高,所以在编译时出现了警告,显示finalize方法已经过时
每个线程都有运行的代码内容,称为线程的任务。而之所以创建线程就是为了去运行指定的任务代码
线程的任务都封装在特定的区域中。例如:主线程运行的任务都定义在main方法中
创建多线程
单线程
//单线程示例
class People{
private String name;//初始化People(String name){
this.name = name;}public void show(){
for(int x = 1;x<11;x++){
System.out.println(name+"-----"+x);}}
}
public class ThreadDemo{
public static void main(String args[]){
People p1 = new People("LILY");People p2 = new People("LUCY");p1.show();p2.show();}
}
创建多线程
1.继承Thread类
2.覆盖run方法
3.创建子类对象就是创建线程对象
4.调用Thread类中的start方法就可以执行线程,并会调用run方法
//继承Thread类
class People extends Thread{
private String name;//初始化People(String name){
this.name = name;}public void show(){
for(int x = 1;x<11;x++){
System.out.println(name+"-----"+x);}}//覆盖run方法public void run(){
show();}
}
public class ThreadDemo{
public static void main(String args[]){
People p1 = new People("LILY");People p2 = new People("LUCY");p1.start();//调用start方法,start会调用run方法,run方法再调用show方法p2.start();}
}
可以看到,代码两次运行后的结果有区别,说明两个线程的运行是由CPU确定的,运行具有不确定性。
获取当前线程对象名
使用Thread类中的getName()
//单线程示例
class People extends Thread{
private String name;//初始化People(String name){
this.name = name;}public void show(){
for(int x = 1;x<11;x++){
System.out.println(getName()+"-----"+name+"-----"+x);}}
}
public class ThreadDemo1{
public static void main(String args[]){
People p1 = new People("LILY");People p2 = new People("LUCY");p1.show();p2.show();}
}
Thread-0,Thread-1仅仅代表了当前对象的名字,不代表线程被开启。
获取当前线程路径,(开启线程)
使用Thread.currentThread()方法
注:需要调用start方法启动线程
class People extends Thread{
private String name;//初始化People(String name){
this.name = name;}public void show(){
for(int x = 1;x<11;x++){
//获取自定义的两个线程当前路径System.out.println(Thread.currentThread().getName()+"-------"+name+"-----"+x);}}//覆盖run方法public void run(){
show();}
}
public class ThreadDemo{
public static void main(String args[]){
People p1 = new People("LILY");People p2 = new People("LUCY");p1.start();//调用start方法,start调用run方法,run方法再调用show方法p2.start();//获取主线程当前路径for(int i = 1;i<11;i++){
System.out.println(Thread.currentThread().getName()+"-----"+i);}}
}
当启动了p1.start()后是继续执行p2.start()还是执行run方法是不确定的,取决于CPU所分配的时间片,如果当前CPU在主线程上,则会继续启动p2。
可以看到,主线程和自定义的两个线程执行的顺序是不一致的,体现了CPU的随机性。
问题:调用start和调用run有什么区别
答:调用run多线程是没有被开启的,此时只有主线程在执行。而调用start是启动run里面自定义的线程对象
线程路径和线程对象名的区别
线程结束的条件是所属线程栈里没有其他方法
线程的几种状态
注:sleep需要指定睡眠时间,单位为毫秒。
创建多线程(推荐)
首先,我们先写一个售票的小例子,假设总共有火车票100张,开启三个窗口去售卖这些车票
class TicketDemo extends Thread{
private int tickets = 100;public void run(){
while(true){
if(tickets>0){
System.out.println(Thread.currentThread().getName()+"-----"+tickets--);}} }
}
public class SaleTicket{
public static void main(String args[]){
TicketDemo t1 = new TicketDemo();TicketDemo t2 = new TicketDemo();TicketDemo t3 = new TicketDemo();//开启三个线程t1.start();t2.start();t3.start();}
}
但是运行结果似乎有一点点的不对劲,我们总共100张票,但是三个线程都输出了相同的数,就是说,一张票在不同的窗口被售卖了三次,这正常吗?明显不正常!
分析代码可以得知,我们在堆中new了三个TicketDemo,每个TicketDemo中都有100张票,但是我们在本例子中只需要一个TicketDemo,那么,如何在实例化一个对象的前提下,开启三个或者多个线程呢
此处需要注意:如果只实例化一个对象,那么当前线程只有2个,即主线程和自定义开启的线程,但要注意的是,多次启用一个线程是非法的,特别是当线程已经结束执行后,不能再重新启动,如果线程已启动,会抛出IllegalThreadStateException
是否可以使用Thread类对象来创建四个线程呢?
public class SaleTicket{
public static void main(String args[]){
Thread t1 = new Thread();Thread t2 = new Thread();Thread t3 = new Thread();//开启三个线程t1.start();t2.start();t3.start(); }
}
运行代码会看到没有输出结果,这是为什么呢?这里需要注意的是,我们使用Thread创建线程,那么start调用的run方法会是父类自身的run方法,而该方法是一个空方法,所以没有任何输出结果。
创建多线程方式二-----实现Runnable接口
1.定义一个类实现Runnable。
2.覆盖Runnable接口中的run方法。
3.通过Thread类创建线程对象,并将实现了Runnable接口的对象作为Thread类的构造函数的参数进行传递。
4.调用start方法,启动线程。
//实现Runnable接口
class TicketDemo implements Runnable{
//extends Thread{
private int tickets = 100;public void run(){
while(true){
if(tickets>0){
System.out.println(Thread.currentThread().getName()+"-----"+tickets--);}} }
}
public class SaleTicket{
public static void main(String args[]){
//TicketDemo t1 = new TicketDemo();//TicketDemo t2 = new TicketDemo();//TicketDemo t3 = new TicketDemo();TicketDemo t = new TicketDemo();Thread t1 = new Thread(t);Thread t2 = new Thread(t);Thread t3 = new Thread(t);//开启三个线程t1.start();t2.start();t3.start(); }
}
实现Runnable接口的好处
1.避免了继承Thread类的单继承的局限性
2.Runnable接口更符合面向对象,将线程单独进行对象的封装
3.Runnable接口降低了线程对象和线程任务的耦合性
多线程的安全问题
CPU时间片的分配可能会导致程序运行时出错
测试代码
//实现Runnable接口
class TicketDemo implements Runnable{
//extends Thread{
private int tickets = 100;public void run(){
while(true){
if(tickets>0){
//让线程在该处暂停try{
Thread.sleep(10);}catch(InterruptedException e){
}System.out.println(Thread.currentThread().getName()+"-----"+tickets--);}} }
}
可以看到,程序执行到最后时,出错了。出错的原因是当某一个线程在执行了if判断之后,CPU将时间片分配给了另一个进程,当时间片又分配给当前进程时,当前进程无需做if判断而直接输出,导致了输出结果异常。
解决方式-----同步代码块
//实现Runnable接口
class TicketDemo implements Runnable{
//extends Thread{
private int tickets = 100;//使用一个现有的类创建对象Object obj = new Object();public void run(){
while(true){
/*使用同步代码块,是一种内部机制,当有线程使用时,其他线程没有该对象的使用权synchronized(对象){需要被同步的代码}同步机制其实就是锁的机制*/synchronized(obj){
if(tickets>0){
//让线程在该处暂停try{
Thread.sleep(10);}catch(InterruptedException e){
}System.out.println(Thread.currentThread().getName()+"-----"+tickets--);}}} }
}
public class SaleTicket{
public static void main(String args[]){
//TicketDemo t1 = new TicketDemo();//TicketDemo t2 = new TicketDemo();//TicketDemo t3 = new TicketDemo();TicketDemo t = new TicketDemo();Thread t1 = new Thread(t);Thread t2 = new Thread(t);Thread t3 = new Thread(t);//开启三个线程t1.start();t2.start();t3.start(); }
}
同步在目前状态下保证了一次只有一个线程在执行,其他进程无法进入。
好处:解决了多线程的安全问题
弊端:降低了效率,但是在可接受的范围,一种以效率换安全的方式
注意:synchronized(对象)中需要用公有的锁,例子中如果换成synchronized(new Obiect()),那么每个进程使用的都是自己的锁,无法做到同步。
同步锁使用的前提是首先判断程序是不是多线程,单线程无需用到同步锁,且多个线程在同步中必须使用同一个锁。