Java的多线程
多线程是Java里一个非常重要的技术,几乎所有用Java开发的软件都会用到这个技术,所以它非常的重要。
1. 多线程是什么?
我们必须先来说另一个概念,那就是进程,大家应该都还比较熟悉,打开我们的window的任务管理器,打开进程栏,里面排列的就是一个个进程,进程也就是我们正在执行的程序,包括系统程序和我们自己运行的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径或者说是控制单元,而线程就是进程中的一个独立的控制单元,它控制着进程的执行,每一个进程中都至少有一个线程,就拿我们的java虚拟机来说,每次启动都会开启我们的主线程,还有底层的垃圾回收器也是会开启的,垃圾回器它也是一个线程,垃圾回收器和主线都在同时运行这就是多线程。
2. 多线程的的作用?
多线程主要就是提高运行效率,想一想如果一个应用程序一次只能做一件事情那该是有多么痛苦,就像我们在用杀毒软件的时候,我们可以一边杀毒,一边清理垃圾,这就是多线程的好处,我们可以同时做很多事情,效率大大增加,在这里我要说一点就是,我们看到的计算机多程序同时运行,其实它们并不是同时在运行,只是因为计算机在各个程序之间进行着高速的切换动作,以至于我们觉得它们是同时再运行。
3. 那我们该怎样来创建多线程呢?
//创建方式一
class demo extends Thread //继承Thread类
{
public void run()//实现run方法
{
for (int i=0;i<100 ;i++ )
{
System.out.println("i="i);
}
}
}
class demo01
{
public static void main(String[] args)
{
Thread t = new demo();//建立线程
t.start();//开启线程
}
}
这是第一种创建方式
创建一个Demo类去继承Thread类,并复写Thread类中run方法,我们需要运行的程序就放到run方法中,然后在主函数中用Thread方法创建Demo类的对象,最后用对象调用start方法启动线程。
//创建方式二
class demo implements Runnable// 实现Runnable接口
{
private int i=100;
public void run()//复写run方法
{
for (int i=0;i<100 ;i++ )
{
System.out.println("i="+i);
}
}
}
class demo02
{
public static void main(String[] args)
{
Runnable r = new demo();//创建Runnable对象
Thread t1 = new Thread(r);//Runnable对象做参数
Thread t2 = new Thread(r);
t1.start();//启动线程
t2.start();
}
}
这是第二种创建方式
创建一个threadDemo类去实现Runnable接口,并复写Runnable接口中run方法,我们需要运行的程序就放到run方法中,然后在主函数中创建threadDemo类的对象,再Thread方法创建对象,把threadDemo类的对象作为参数的形式传到Thread的构造函数的参数里,最后用Thread对象调用start方法启动线程。
两种线程创建方式的比较:
这两种方式都可以创建多线程,继承Thread类创建多线程就不能去继承别的类,而且Thread类创建多个线程需要创建多个对象。而去实现Runnable接口的方法创建的时候,可以用这个类创建一个对象用Thread类同时开启多个线程,为了提高程序的拓展性,我们一般会用第二种方法创建线程。
4.多线程存在的安全问题和解决方案(同步和锁)
通过一个买票程序来说明安全问题。
class demorunnable implements Runnable
{
private int i=100;//卖100张票
public void run()
{
while(true)
{
if(i>0)//判断票是否卖完
{
try{Thread.sleep(10);}catch(Exception e){}
//为了看到安全问题 用手动的方式让线程在这里停10毫秒
System.out.println(Thread.currentThread().getName()+"---- "+(i--));
//每个线程都带着自己的编号和票号一起打印,打印之后票号递减
}
else//卖完就结束
return;
}
}
}
class demo03
{
public static void main(String[] args)
{
Runnable r = new demorunnable();//创建对象
Thread t1 = new Thread(r);//用3个窗口来买票
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t1.start();//3个线程启动
t2.start();
t3.start();
}
}
我们通过程序的运行结果来说明问题,运行的结果里出现了0和-1号票,在现实中是不能出现种问题的!
现在我就来分析一下,这里除了主线程之外我们另外开启了3条买票线程,这3条线程是怎么运作的呢?cup只能执行一个线程(我们这里不考虑多核CPU),所以在程序运行的时候,cup就是在不停的切换这3条线程执行,它们都有可能被cup执行到。这个程序我们做了一个睡眠sleep动作,让每个线程从这里过的时候就停一会,当到最后0号线程拿着1号票进来,然后就停在那里,cup把执行权给了 1号线程,1号线程判断票号为1,也进来了,同样的2号线程也进来了,然后它们又一个个打印了,第一个打印的就输出1号票,接下来这个线程就输出了0号票,最后的就输出了-1号票!
怎么来解决这个问题呢?
那我们就要说到另外一个概念就是同步和锁,还是刚刚那段代码,我们加一个同步再看看!
//一个多线程卖票的程序(已经加了同步)
class demorunnable implements Runnable
{
private int i=500;//卖100张票
public void run()
{
while(true)
{
synchronized(this)//这个就是同步代码块,括号里的this就是锁
{
if(i>0)//判断票是否卖完
{
try{Thread.sleep(10);}catch(Exception e){}
//为了看到安全问题 用手动的方式让线程在这里停10毫秒
System.out.println(Thread.currentThread().getName()+"---- "+(i--));
}
else//卖完就结束
return;
}
}
}
}
class demo03
{
public static void main(String[] args)
{
Runnable r = new demorunnable();//创建对象
Thread t1 = new Thread(r);//用3个窗口来买票
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t1.start();
t2.start();
t3.start();
}
}
同步代码块的格式就是这样子:
synchronized(对象)
{
需要同步的代码
}
大括号里写我们需要同步的代码,小括号里的对象就是锁,他必须是一个对象,每一个线程执行到这里时,会先拿锁进去,然后进去读同步代码块里面的代码,如果这时别的线程夺取到执行权也来到同步代码这里,它也会判断锁,判断这个锁已经被拿进去了,它就会释放执行权,让别的线程执行,就这样一直要等到同步代码里面的线程执行完里面的语句出来之后,别的线程才能拿到锁,进到同步代码块里去执行。
还有一点要说明的就是,要想达到同步的效果,这几个线程必须用同一个锁(也就是同一个对象),以我们这个买票程序为例:我用的是一个this锁,就是demorunnable类的一个对象,因为我3条线程用的都是这一个demorunnable对象,所以可以达到同步的效果,特别注意的是同步代码块如果是在静态函数中;还有就是同步代码块中的代码一定是需要同步的代码,也就是那些操作共同变量的语句,不需要同步的代码也放在里面的话会影响效率。
同步的另一种写法:同步函数!
如果一个函数里面所有的语句都是需要同步的话,就把synchronized关键字直接写到函数上,如public synchronized void(){}这样子整个函数都会变成同步的。
有人就会问这个同步函数怎么没有锁?有锁,是有锁的,所就是调用这个函数的对象,也就是this。有一点要注意,静态函数的锁不是对象,因为静态是随着类的加载而加载,那个时候还没有对象,只有类,所以锁就是用了该类的字节码文件对象,类名.class。
5.线程的五种状态和等待唤醒机制
5.1这五种状态分别是:创建、运行、临时阻塞、冻结(又分为睡眠和等待两种状态)、消亡五种状态,我就结合下面这张图来说一说五种状态并介绍一些线程的方法。
首先是创建就不多说了,运行状态就是线程抢到了cpu的执行权运行了,消亡就是线程直接被干掉了,用的是stop方法,这种干掉就是结束了线程不能再被运行;
冻结状态是用sleep和wait方法,sleep方法里面一般会加一个long类型的参数,就是冻结的时间,时间一结束就会去到临时状态等着cpu的执行权;
wait方法和notify方法是一对,当线程用了wait方法等待之后就会冻结,这个时候必须要notify方法来把它唤醒 ,唤醒之后又会回到临时状态去等待cpu的执行权;因为cpu(单核)一次只能运行一个线程,所以如果程序里有3条线程,那么肯定只有一条线程在运行,另外两条线程如果没有在冻结状态的话,肯定就在临时阻塞状态里等待执行!
5.2线程的等待唤醒机制
为什么会出现这个机制?这个机制有什么用?
我们通过一个生产消费手机的程序来说明这个问题,程序是这样子的,我们会创建多个线程去生产手机,然后又会创建多个线程去消费手机,生产方法和消费方法我都用不同的代码去实现,它们都是各自同步的,但是这里也会出现一个安全问题,就是 生产和消费有多个,它们都在抢夺cpu的执行权,这个时候可能就是生产出一个手机之后,消费线程去消费,消费一次之后呢,消费线程又抢到的执行权再去消费一次,这样子就是变成一个手机卖给了两个人,这时我们就需要用等待唤醒机制了,让生产线程生产一个手机之后就等待,等到消费线程消费之后让消费线程唤醒生产线程,消费线程自己等待,就是这样子让它们相互等待唤醒以确保安全问题。
下面就是代码
/*手机的生产与消费*/
class demo04
{
public static void main(String[] args)
{
product p = new product();//产品对象
runDemo r1 =new runDemo(p,"苹果公司");//两个生产者
runDemo r2 =new runDemo(p,"小米公司");
conDemo r3 =new conDemo(p,"土豪");//两个消费者
conDemo r4 =new conDemo(p,"屌丝");
Thread t1 = new Thread(r1);//加入
Thread t2 = new Thread(r2);
Thread t3= new Thread(r3);
Thread t4 = new Thread(r4);
t1.start();//4个线程开启
t2.start();
t3.start();
t4.start();
}
}
class runDemo implements Runnable//生产者
{
private product p;
private String name;
runDemo(product p,String name)
{
this.name = name;
this.p = p;
}
public void run()//复写run方法
{
while(true)//消费
p.pro(name);//生产手机
}
}
class conDemo implements Runnable//消费者
{
private product p;
private String name;
conDemo(product p,String name)
{
this.name = name;
this.p = p;
}
public void run()//复写run方法
{
while(true)//循环
p.con(name);//消费手机
}
}
class product// 产品
{
private String name;//生产者和消费者的名字
private int i = 0;//商品的个数
private boolean b=false;//判断标记,用来判断是否等待和唤醒
public void pro(String name)//产品生产的方法
{
synchronized(this)//同步代码
{
while(b)//判断是否要等待 生产和消费的标记相反
try{wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+name+"生产了商品---"+(++i));
b=true;//修改标记
notifyAll();
}
}
public void con(String name)//产品消费的方法
{
synchronized(this)//同步代码
{
while(!b)//判断是否要等待 生产和消费的标记相反
try{wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+name+"消费了商品---"+i);
b=false;//修改标记
notifyAll();
}
}
}