黑马程序员---多线程
2015-07-01 22:12
603 查看
-----------android培训、java培训、java学习型技术博客、期待与您交流!---------
进程:正在执行中的程序,每个进程执行都有一个执行的顺序,该顺序就是一个执行路径,或者叫一个控制单元。
线程:就是进程中的一个独立的公职单元,线程在控制着进程的执行。
只要进程中有一个线程在执行,进程就不会结束,一个进程中至少有一个线程。
多线程:在java虚拟机启动的时候会有一个进程java.exe,该进程中至少有一个线程负责java程序的执行。
而且这个线程运行的代码存在于main方法中。该线程称之为主线程。JVM启动除了执行一个主线程,
还有负责垃圾回收机制的线程。像这种在一个进程中有多个线程执行的方式,就叫做多线程。
多线程存在的意义
多线程的出现能让程序出现同时运行效果(原来没学多线程的时候我以为程序是同时运行的,
学完之后才发现不是,等一下我会讲解CPU的运行原理)可以提高程序执行效率。
例如:在java.exe进程执行主线程时,如果程序代码特别多,在堆内存中产生了很多对象。
而对象调用完后,就成了垃圾。如果垃圾过多可能发生内存溢出的情况,如果只有一个线程工作的话,程序的执行将会很低效。
如果有另一个线程帮助处理的话,比如垃圾回收机制线程来帮助回收垃圾的话,程序的运行将变得更有效率。
创建线程有两种方式
继承方式和实现方式,继承就是继承Thread类,实现就是实现Runnable接口,我们先来看继承方式。
继承方式:
通过对API的查找,java已经提供了对线程这类事物的描述,就是Thrad类
创建线程的第一种方式:继承Thread类。
1. 定义类继承Thread
2. 复写Thread类中的run方法。
目的:将线程要执行的代码储存在run方法中,让程序运行。
3. 创建类的实例对象,就相当于创建了一个线程。
4. 调用线程的start方法,该方法有两个作用
a. 启动线程
b. 调用run方法
start:调用到底层让控制单元去执行的动作。
run:封装线程要运行的代码
注意:调用线程的start方法是开启线程并执行该线程的run方法。
而直接调用run方法仅仅是对象调用方法,而线程创建了,并没有运行
为什么要覆盖run方法呢?
Thread类用于描述线程,Thread类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法。
也就是说Thread类中的run方法,用于存储线程要运行的代码。
下面我们来看一个使用继承方法,创建线程的实例
运行结果
![](http://img.blog.csdn.net/20150701221812592?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvUGFuZGEwMzE4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
注:Thread.currentThread()== this 这种获取线程对象的方法不建议使用,因为this不一定通用。
直接用Thread.currentThread()就好,因为它是标准通用的。
为什么每次运行的结果都不一样呢?
因为多个线程都获取CPU的执行权,CPU执行到谁谁就运行。
明确点的说就是在某个时刻只能一个程序在运行(多核除外),CPU在做着快速的切换,以达到看上去是同时运行的效果,
我们可以形象的把多线程的运行形容为在互相抢夺CPU的执行权,
这就是多线程的一个特性:随机性。(谁抢到谁执行,至于执行多长时间,CPU说了算)
创建线程的第二种方式,实现方式
使用继承方式有一个弊端,那就是如果该类本来就继承了其他父类,就无法通过Thread类来创建线程了。
这样就有了第二种创建线程的方式:实现Runnable接口,并复写run方法
1. 定义类实现Runnable接口
2. 覆盖Runna接口中的run方法,将线程要执行的代码存放在run方法中。
3. 通过Thread类创建线程对象。
4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
为什么要将Runnable接口的子类对象传给Thread的构造函数呢?
因为,自定义的run方法所属的对象是Runnable接口的子类对象,所以要让线程去执行指定对象的run方法,
就必须明确该run方法的所属对象。
5. 调用Thread类的start方法,开启线程,start会自动调用 Runnable接口子类的run方法。
实现方式好处:避免了单继承的局限性。在定义线程时,建议使用实现方式。
实例程序
运行结果
![](http://img.blog.csdn.net/20150701223332586?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvUGFuZGEwMzE4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
在这个程序的运行结果中出现了Thread-0这类的名称,通过查看源代码我们知道这是通过获取线程名得来的,
原来线程有自己的默认名称:Thread—编号(编号从0开始)
实现方法和继承方法有什么区别呢?
实现方法的好处:避免了单继承的局限性,在定义线程时,建议使用实现方法
两种方式的区别:线程代码存放的位置不一样。
继承Thread:线程代码存放在Thread子类的run方法中。
实现Runnable:线程代码存放在Runnable接口的子类的run方法中。
线程的状态
线程的几种状态分别为
被创建:等待被start方法被调用启动。
运行状态:具有执行资格和执行权。
临时状态(阻塞状态):具备执行资格,但是没有执行权。
冻结状态:有两个状态
a. 睡眠:sleep(time)方法
b. 等待:wait()方法。放弃了执行资格。
当sleep方法时间到或者调用到notify()方法时,获得执行资格,变为临时状态。
变为临时状态后,如果CPU空闲再执行。
消亡状态:run()方法结束,或者遇到stop()方法。
总结:没有执行资格是冻结状态,有执行资格没有执行权是阻塞状态,执行权执行资格都有是运行状态。
线程的状态图示
![](http://img.blog.csdn.net/20150701223935098?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvUGFuZGEwMzE4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
虽然这不是一个安全性的问题,但是如果真的在做项目的过程中,出现一点小问题都是很可怕的。
而且如果我们在卖鞋程序的输出语句前加入:
try{Thread.sleep(10);}catch(Exception e){}(因为sleep可能会发生异常,所以要try catch处理)代码,让线程睡一会儿,
运行出的结果可能会发生如下图所示的情况
会出现0,-1,-2的号,这就表明多线程出现了很严重的安全问题,我们必须要解决
![](http://img.blog.csdn.net/20150701224246900?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvUGFuZGEwMzE4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
问题的原因
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,
另一个线程就参与进来执行,导致了共享数据的错误。
解决办法
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
在java中对于多线程的安全问题提供了专业的解决方式——synchronized(同步)
同步有两种方式。
1. 同步代码块。
2. 同步函数。
都是利用synchronized关键字实现的
先来介绍同步代码块
格式:
Synchronized(对象){
需要被同步的代码;
}
判断那些代码需要同步,就看那些语句在操作共享数据。
同步可以解决安全问题的原因就在对象上。对象如同锁,持有锁的线程可以在同步中执行,
没有持有锁的线程即使获取了CPU的执行权也进不去,因为没有获取锁。
下面我们就给卖鞋那号的程序加上同步代码块,再来看运行结果
给程序加入同步代码块
运行结果
![](http://img.blog.csdn.net/20150701225142509?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvUGFuZGEwMzE4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
现在就可以一直卖到最后一张票了,在截图中只能看到Thread-3线程在运行,是因为我的电脑是双核的,比较不容易看到线程交替。
虽然鞋号数已经调到了1000,还是看不出来,所以只截了卖到最后一张的截图,望老师见谅,嘿嘿。
同步函数
格式
把synchronized加在函数上public后即可。
同步函数用的是哪一个锁呢?
函数需要被对象调用,那么函数都有一个所属对象的引用,就是this。所以同步函数的锁是this。
再来看刚才卖鞋的程序用同步代码块来使它安全。
运行结果
![](http://img.blog.csdn.net/20150701230124356?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvUGFuZGEwMzE4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
同步的前提
1. 必须有两个以上的线程。
2. 必须是多个线程使用同一个锁。
必须保证同步中只能有一个线程在运行
同步的好处和弊端
好处:解决了多线程的安全问题
弊端:多个线程都要判断锁,较为消耗资源
如何寻找线程中的安全问题
1. 明确哪些代码是多线程运行代码。
2. 明确共享数据(一般成员都是,对象也是)
3. 明确多线程运行代码中那些语句是操作共享数据的。
静态函数的同步方式
如果同步函数被静态修饰后,使用的锁是什么呢?
通过验证发现,不是this,因为静态方法中不可以定义this,静态进内存时,内存中没有本类对象。
但是一定有该类对应的字节码文件对象。类名.class。该对象的类型是class,
静态的同步方法,使用的锁是该方法所在类的字节码文件对象。类名.class
同步的经典懒汉式加上同步
用程序来体现死锁:面试题
结果:程序卡主,不能继续运行
![](http://img.blog.csdn.net/20150701232840390?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvUGFuZGEwMzE4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
下面我们来看一个线程间通信的例子
运行结果
![](http://img.blog.csdn.net/20150701234408622?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvUGFuZGEwMzE4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
下面有几个小知识
1. wait(); notify() ; notifyAll() 都使用在同步中,因为要对持有监视器(锁)的线程操作。
为什么这些操作线程的方法要定义在Object类中呢?
因为这些方法在操作同步线程时,都必须要标识他们所操作的线程持有的锁。
只有同一个锁上的被等待线程,可以被同一个锁notify()唤醒,不可以对不同锁中的线程进行等待唤醒。
也就是说,等待和唤醒必须是同一个锁,而锁可以是任意对象,所以可以被任意对象调用的方法定义在Object中。
2. wait(),sleep(time)有什么区别?
wait():释放CPU执行权,释放锁。
sleep():释放CPU执行权,不释放锁。
3. 为什么要定义notifyAll()?
因为在需要唤醒对方线程时,如果只使用notify(),会容易出现只唤醒本方线程的情况,导致程序所有的线程都等待。
因为notify()只通知队列中的第一个线程,而notifAll()通知的是等待队列中的所有线程
多线程的升级解决方案示例
运行结果
![](http://img.blog.csdn.net/20150701235404967?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvUGFuZGEwMzE4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
那么要如何停止线程呢?
只有一种,run()方法结束。开启多线程运行,运行代码通常是循环结构,只要控制住循环,就可以让run方法结束,也就是线程结束。
如下面的代码:
public voidrun() {
while(flag) {
System.out.println(Thread.currentThread().getName()+"-------run");
}
}
使循环条件为一个变量,等程序运行一段时间后,只要在主函数,或者除了这个线程中的其他线程中
将false赋给标记flag,run方法就会结束,线程也会停止。
但是会出现特殊状况:当线程处于冻结状态,就不会读取到标记,那么线程就不会结束。
当没有指定的方式让冻结恢复到运行状态时,就需要对冻结进行清除,强制让线程恢复到运行状态中来
这样就可以操作标记,让线程结束。Thread类中提供了该方法 interrupt()
特出情况示例
运行结果
![](http://img.blog.csdn.net/20150702000657165?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvUGFuZGEwMzE4/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
让主函数线程代码执行完时,冻结的线程执行结束。
什么时候写多线程?
当某些代码需要同时被执行时,就需要用单独的线程进行封装。
2. setPriority():用来设置优先级:
MAX_PRIORITY 最高优先级10
MIN_PRIORITY 最低优先级1
NORM_PRIORITY 分配给线程的默认优先级5
优先级就是抢资源的频率,越大抢到CPU执行权的机会就越大
3. yield():暂停当前正在执行的线程对象,并执行其他线程。临时释放,稍微减缓线程的运行,而且能达到平均运行效果。
多线程
要理解多线程就要先了解线程,了解线程就要先了解进程,下面跟着我的博客学习多线程吧进程:正在执行中的程序,每个进程执行都有一个执行的顺序,该顺序就是一个执行路径,或者叫一个控制单元。
线程:就是进程中的一个独立的公职单元,线程在控制着进程的执行。
只要进程中有一个线程在执行,进程就不会结束,一个进程中至少有一个线程。
多线程:在java虚拟机启动的时候会有一个进程java.exe,该进程中至少有一个线程负责java程序的执行。
而且这个线程运行的代码存在于main方法中。该线程称之为主线程。JVM启动除了执行一个主线程,
还有负责垃圾回收机制的线程。像这种在一个进程中有多个线程执行的方式,就叫做多线程。
多线程存在的意义
多线程的出现能让程序出现同时运行效果(原来没学多线程的时候我以为程序是同时运行的,
学完之后才发现不是,等一下我会讲解CPU的运行原理)可以提高程序执行效率。
例如:在java.exe进程执行主线程时,如果程序代码特别多,在堆内存中产生了很多对象。
而对象调用完后,就成了垃圾。如果垃圾过多可能发生内存溢出的情况,如果只有一个线程工作的话,程序的执行将会很低效。
如果有另一个线程帮助处理的话,比如垃圾回收机制线程来帮助回收垃圾的话,程序的运行将变得更有效率。
创建线程有两种方式
继承方式和实现方式,继承就是继承Thread类,实现就是实现Runnable接口,我们先来看继承方式。
继承方式:
通过对API的查找,java已经提供了对线程这类事物的描述,就是Thrad类
创建线程的第一种方式:继承Thread类。
1. 定义类继承Thread
2. 复写Thread类中的run方法。
目的:将线程要执行的代码储存在run方法中,让程序运行。
3. 创建类的实例对象,就相当于创建了一个线程。
4. 调用线程的start方法,该方法有两个作用
a. 启动线程
b. 调用run方法
start:调用到底层让控制单元去执行的动作。
run:封装线程要运行的代码
注意:调用线程的start方法是开启线程并执行该线程的run方法。
而直接调用run方法仅仅是对象调用方法,而线程创建了,并没有运行
为什么要覆盖run方法呢?
Thread类用于描述线程,Thread类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法。
也就是说Thread类中的run方法,用于存储线程要运行的代码。
下面我们来看一个使用继承方法,创建线程的实例
/* 创建两个线程,和主线程交替运行。 */ //定义一个ThreadA类继承Thread class ThreadA extends Thread { ThreadA(String name) { //使用super方法,访问父类构造函数 super(name); } //复写run方法写入要执行的语句 public void run() { for(int x=0;x<80;x++) //通过Thread.currentThread方法获得当前执行线程对象的引用 //再用getName方法获得这个正在运行线程的名称,以示区分 System.out.println(Thread.currentThread().getName()+"..run..."+x); } } class ThreadTest { public static void main(String[] args) { //创建ThreadA的匿名对象,并传入name参数,开启自定义的第一个线程 new ThreadA("线程11111111————————————").start(); //创建ThreadA的匿名对象,并传入name参数,开启自定义的第一个线程 new ThreadA("线程22222*******").start(); //编写主线程要执行的代码 for(int x=0;x<170;x++) System.out.println("Hello World!"); } }
运行结果
注:Thread.currentThread()== this 这种获取线程对象的方法不建议使用,因为this不一定通用。
直接用Thread.currentThread()就好,因为它是标准通用的。
为什么每次运行的结果都不一样呢?
因为多个线程都获取CPU的执行权,CPU执行到谁谁就运行。
明确点的说就是在某个时刻只能一个程序在运行(多核除外),CPU在做着快速的切换,以达到看上去是同时运行的效果,
我们可以形象的把多线程的运行形容为在互相抢夺CPU的执行权,
这就是多线程的一个特性:随机性。(谁抢到谁执行,至于执行多长时间,CPU说了算)
创建线程的第二种方式,实现方式
使用继承方式有一个弊端,那就是如果该类本来就继承了其他父类,就无法通过Thread类来创建线程了。
这样就有了第二种创建线程的方式:实现Runnable接口,并复写run方法
1. 定义类实现Runnable接口
2. 覆盖Runna接口中的run方法,将线程要执行的代码存放在run方法中。
3. 通过Thread类创建线程对象。
4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
为什么要将Runnable接口的子类对象传给Thread的构造函数呢?
因为,自定义的run方法所属的对象是Runnable接口的子类对象,所以要让线程去执行指定对象的run方法,
就必须明确该run方法的所属对象。
5. 调用Thread类的start方法,开启线程,start会自动调用 Runnable接口子类的run方法。
实现方式好处:避免了单继承的局限性。在定义线程时,建议使用实现方式。
实例程序
/* 使用实现Runnable接口的方法,创建线程 现在想买好鞋太难了,都得排号,因为鞋太少价钱又被炒上去了,100个号可能就为了抽一双鞋 现在我就来模拟一个拿号系统,四个人发,就相当于4个线程。 */ class Shoes implements Runnable//extends Thread { //有100个号 private int shoesNumber = 100; //复写run方法 public void run() { while(true) { if(shoesNumber>0) { //输出第几个人发出去的号,并且每卖出去一个号减一 System.out.println(Thread.currentThread().getName()+"...发号 "+ shoesNumber--); } } } } class ShoesDemo { public static void main(String[] args) { //创建Runnable接口的子类对象 Shoes s = new Shoes(); //将Runnable接口的子类对象作为参数传递给Thread类的构造函数,创建线程 Thread t1 = new Thread(s); Thread t2 = new Thread(s); Thread t3 = new Thread(s); Thread t4 = new Thread(s); //启动线程 t1.start(); t2.start(); t3.start(); t4.start(); } }
运行结果
在这个程序的运行结果中出现了Thread-0这类的名称,通过查看源代码我们知道这是通过获取线程名得来的,
原来线程有自己的默认名称:Thread—编号(编号从0开始)
实现方法和继承方法有什么区别呢?
实现方法的好处:避免了单继承的局限性,在定义线程时,建议使用实现方法
两种方式的区别:线程代码存放的位置不一样。
继承Thread:线程代码存放在Thread子类的run方法中。
实现Runnable:线程代码存放在Runnable接口的子类的run方法中。
线程的状态
线程的几种状态分别为
被创建:等待被start方法被调用启动。
运行状态:具有执行资格和执行权。
临时状态(阻塞状态):具备执行资格,但是没有执行权。
冻结状态:有两个状态
a. 睡眠:sleep(time)方法
b. 等待:wait()方法。放弃了执行资格。
当sleep方法时间到或者调用到notify()方法时,获得执行资格,变为临时状态。
变为临时状态后,如果CPU空闲再执行。
消亡状态:run()方法结束,或者遇到stop()方法。
总结:没有执行资格是冻结状态,有执行资格没有执行权是阻塞状态,执行权执行资格都有是运行状态。
线程的状态图示
线程安全问题
刚才那个卖鞋的程序,最后一个号不是1,而是别的数字。当然因为程序运行结果都不一样,不一定是哪个数字,反正不是1。虽然这不是一个安全性的问题,但是如果真的在做项目的过程中,出现一点小问题都是很可怕的。
而且如果我们在卖鞋程序的输出语句前加入:
try{Thread.sleep(10);}catch(Exception e){}(因为sleep可能会发生异常,所以要try catch处理)代码,让线程睡一会儿,
运行出的结果可能会发生如下图所示的情况
会出现0,-1,-2的号,这就表明多线程出现了很严重的安全问题,我们必须要解决
问题的原因
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,
另一个线程就参与进来执行,导致了共享数据的错误。
解决办法
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
在java中对于多线程的安全问题提供了专业的解决方式——synchronized(同步)
同步有两种方式。
1. 同步代码块。
2. 同步函数。
都是利用synchronized关键字实现的
先来介绍同步代码块
格式:
Synchronized(对象){
需要被同步的代码;
}
判断那些代码需要同步,就看那些语句在操作共享数据。
同步可以解决安全问题的原因就在对象上。对象如同锁,持有锁的线程可以在同步中执行,
没有持有锁的线程即使获取了CPU的执行权也进不去,因为没有获取锁。
下面我们就给卖鞋那号的程序加上同步代码块,再来看运行结果
给程序加入同步代码块
/* 使用实现Runnable接口的方法,创建线程 现在想买好鞋太难了,都得排号,因为鞋太少价钱又被炒上去了,100个号可能就为了抽一双鞋 现在我就来模拟一个拿号系统,四个人发,就相当于4个线程。 */ class Shoes implements Runnable//extends Thread { //有1000个号 private int shoesNumber = 1000; //创建一个对象,作为锁 Object obj = new Object(); //复写run方法 public void run() { while(true) { //创建同步代码块obj对象为锁,把操作共享数据的代码放入同步代码块中 synchronized(obj) { if(shoesNumber>0) { try{Thread.sleep(10);}catch(Exception e){} //输出第几个人发出去的号,并且每卖出去一个号减一 System.out.println(Thread.currentThread().getName()+"...发号 "+ shoesNumber--); } } } } } class ShoesDemo { public static void main(String[] args) { //创建Runnable接口的子类对象 Shoes s = new Shoes(); //将Runnable接口的子类对象作为参数传递给Thread类的构造函数,创建线程 Thread t1 = new Thread(s); Thread t2 = new Thread(s); Thread t3 = new Thread(s); Thread t4 = new Thread(s); //启动线程 t1.start(); t2.start(); t3.start(); t4.start(); } }
运行结果
现在就可以一直卖到最后一张票了,在截图中只能看到Thread-3线程在运行,是因为我的电脑是双核的,比较不容易看到线程交替。
虽然鞋号数已经调到了1000,还是看不出来,所以只截了卖到最后一张的截图,望老师见谅,嘿嘿。
同步函数
格式
把synchronized加在函数上public后即可。
同步函数用的是哪一个锁呢?
函数需要被对象调用,那么函数都有一个所属对象的引用,就是this。所以同步函数的锁是this。
再来看刚才卖鞋的程序用同步代码块来使它安全。
/* 使用实现Runnable接口的方法,创建线程 现在想买好鞋太难了,都得排号,因为鞋太少价钱又被炒上去了,100个号可能就为了抽一双鞋 现在我就来模拟一个拿号系统,四个人发,就相当于4个线程。 */ class Shoes implements Runnable//extends Thread { //有100个号 private int shoesNumber = 1000; public void run()//复写run方法 { //调用同步方法 show(); } //将sysnchronized关键字加入函数中 public synchronized void show() { while(true) { if(shoesNumber>0) { try{Thread.sleep(10);}catch(Exception e){} //输出第几个人发出去的号,并且每卖出去一个号减一 System.out.println(Thread.currentThread().getName()+"...发号 "+ shoesNumber--); } } } } class ShoesDemo2 { public static void main(String[] args) { //创建Runnable接口的子类对象 Shoes s = new Shoes(); //将Runnable接口的子类对象作为参数传递给Thread类的构造函数 Thread t1 = new Thread(s);//创建线程; Thread t2 = new Thread(s); Thread t3 = new Thread(s); Thread t4 = new Thread(s); t1.start();//启动线程 t2.start(); t3.start(); t4.start(); } }
运行结果
同步的前提
1. 必须有两个以上的线程。
2. 必须是多个线程使用同一个锁。
必须保证同步中只能有一个线程在运行
同步的好处和弊端
好处:解决了多线程的安全问题
弊端:多个线程都要判断锁,较为消耗资源
如何寻找线程中的安全问题
1. 明确哪些代码是多线程运行代码。
2. 明确共享数据(一般成员都是,对象也是)
3. 明确多线程运行代码中那些语句是操作共享数据的。
静态函数的同步方式
如果同步函数被静态修饰后,使用的锁是什么呢?
通过验证发现,不是this,因为静态方法中不可以定义this,静态进内存时,内存中没有本类对象。
但是一定有该类对应的字节码文件对象。类名.class。该对象的类型是class,
静态的同步方法,使用的锁是该方法所在类的字节码文件对象。类名.class
同步的经典懒汉式加上同步
/* 加入了同步的懒汉单例设计模式 */ class Single { //定义私有,静态的类的引用,类加载时引用就存在,而且不能被外界访问 private static Single s = null; //定义私有的构造函数,让用户不能创建对象 private Single(){} //定义获取对象方法,类名可以直接调用,所以是静态的 public static void getInstance() { //判断对象的引用是否指向null,这里加了双重判断。 //在多线程并发的情况下,每个线程要创建对象都要判断一下锁,效率比较低,为了提高效率加入双重判断。 //如果第一个线程已经成功的获取了实例对象,Single类的引用就不指向空了,就不用,每次都判断锁了 if(s == null) { //使用同步代码块,让线程同步,锁为Single的字节码 synchronized(Single.class) { //这里面才是真正需要被同步的代码 //如果Single类的引用指向null if(s == null) //创建Single的实例对象,并指向s引用型变量 s = new Single(); } } //返回类的引用 return s; } }
死锁
在同步中嵌套同步时可能出现死锁现象用程序来体现死锁:面试题
/* 这是一个死锁程序 这个程序中会出现两个线程。 通过给构造函数传入不同标记的方式,用if else代码块控制线程运行的代码。 一号线程:A锁的同步代码块套着B锁的同步代码块 二号线程:B锁的同步代码块套着A锁的同步代码块 会出现死锁的情况 */ class Test implements Runnable { private boolean flag; //建立对象时接收boolean类型的数据 Test(boolean flag) { this.flag = flag; } //复写run方法 public void run() { if(flag) { while(true) { synchronized(MyLock.lockA)//A锁 { System.out.println(Thread.currentThread().getName()+"...运行if代码块 lockA"); synchronized(MyLock.lockB)//B锁 { System.out.println(Thread.currentThread().getName()+"..运行if代码块 lockB"); } } } } else { while(true) { synchronized(MyLock.lockB)//B锁 { System.out.println(Thread.currentThread().getName()+"..运行else代码块 lockB"); synchronized(MyLock.lockA)//A锁 { System.out.println(Thread.currentThread().getName()+".....运行else代码块 lockA"); } } } } } } //创建一个类用于放锁 class MyLock { static Object lockA = new Object(); static Object lockB = new Object(); } class DeadLockTest { public static void main(String[] args) { //创建线程,并把标记作为参数传入实现Runnable接口的类的匿名对象中 Thread t1 = new Thread(new Test(true)); Thread t2 = new Thread(new Test(false)); //开启线程 t1.start(); t2.start(); } }
结果:程序卡主,不能继续运行
线程间的通信
其实就是多个线程在操作同一个资源,但是操作的动作不同。下面我们来看一个线程间通信的例子
/* 这是一个线程之间通信的例子 两个线程 一个往资源里面存东西,一个从资源里面取东西。存一个取一个 */ //定义一个手机类 class Phone { //手机品牌 String brand; //手机耐久度 String durable; //标记 boolean flag = false; } //创建往资源里存东西的类实现Runnable端口 class Input implements Runnable { private Phone p ; //Input类的构造函数接收Phone对象 Input(Phone p) { this.p = p; } //复写run方法 public void run() { //定义标记,为了交替打印用 int x = 0; //定义循环 while(true) { //同步代码块,手机对象为锁 synchronized(p) { //判断手机类中的标记 if(p.flag) //使线程等待 try{p.wait();}catch(Exception e){} //给Phone对象赋值 if(x==0) { p.brand="苹果"; p.durable="耐久度不好,容易碎屏"; } else { p.brand="诺基亚"; p.durable="耐久度好,能砸核桃"; } //控制交替打印 x = (x+1)%2; //改变手机标记 p.flag = true; //唤醒等待的第一个线程,因为等下我只会创建两个线程,所以使用notify()就好 //自己睡了,唤醒对方。 p.notify(); } } } } //创建从资源里往外拿东西的类实现Runnable接口 class Output implements Runnable { private Phone p ; //参数接收Phone类对象 Output(Phone p) { this.p = p; } //复写run方法 public void run() { //定义循环 while(true) { //同步代码块,Phone对象为锁 synchronized(p) { //判断标记,与Input类判断相反 if(!p.flag) //使线程等待 try{p.wait();}catch(Exception e){} //获取P对象的值并打印 System.out.println(p.durable+"...."+p.durable); //改变Phone类的标记 p.flag = false; //唤醒等待的第一个线程。 p.notify(); } } } } class InputOutputDemo { public static void main(String[] args) { //创建手机对象 Phone p = new Phone(); //创建读取资源和存入资源的对象,并将手机对象作为实际参数传入构造函数中 Input in = new Input(p); Output out = new Output(p); //将Runnable的子类对象作为参数传入Thread的构造函数中,创建线程并启动线程。 new Thread(in).start(); new Thread(out).start(); } }
运行结果
下面有几个小知识
1. wait(); notify() ; notifyAll() 都使用在同步中,因为要对持有监视器(锁)的线程操作。
为什么这些操作线程的方法要定义在Object类中呢?
因为这些方法在操作同步线程时,都必须要标识他们所操作的线程持有的锁。
只有同一个锁上的被等待线程,可以被同一个锁notify()唤醒,不可以对不同锁中的线程进行等待唤醒。
也就是说,等待和唤醒必须是同一个锁,而锁可以是任意对象,所以可以被任意对象调用的方法定义在Object中。
2. wait(),sleep(time)有什么区别?
wait():释放CPU执行权,释放锁。
sleep():释放CPU执行权,不释放锁。
3. 为什么要定义notifyAll()?
因为在需要唤醒对方线程时,如果只使用notify(),会容易出现只唤醒本方线程的情况,导致程序所有的线程都等待。
因为notify()只通知队列中的第一个线程,而notifAll()通知的是等待队列中的所有线程
JDK1.5中提供了多线程的升级解决方案
将同步synchronized替换成显示的Lock操作,将Object中的wait() ; notify() ; notifyAll()替换成condition对象,该对象可以通过Lock锁进行获取。多线程的升级解决方案示例
import java.util.concurrent.locks.*; /* 想想一个好玩儿一点儿的例子,想不出来啊,就用毕老师讲课的商品例子吧,我尽力了! 创建四个线程。 两个生产者,生产一次就等待消费一次 两个消费者,等待生产者生产一次就消费一次 */ //资源类 class Resource { private String name; private int count = 1; //标记 private boolean flag = false; //多态,父类引用指向子类对象 private Lock lock = new ReentrantLock(); //创建两个Condition对象,用于控制等待或唤醒本方与对方线程 private Condition condition_pro = lock.newCondition(); private Condition condition_con = lock.newCondition(); //生产方法 public void set(String name)throws InterruptedException { lock.lock(); //l锁 try { while(flag) //重复判断标识,确认是否生产 condition_pro.await(); //本方线程等待 this.name = name+"...."+count++;//生产 //打印生产信息 System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name); flag = true;//控制标记 condition_con.signal(); //唤醒对方线程 } finally { lock.unlock();//释放锁的动作一定要执行。 } } //消费方法 public void out()throws InterruptedException { lock.lock();//锁 try { while(!flag) //重复判断标识,确认是否可以消费 condition_con.await(); //本方线程等待 //打印消费信息 System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name); flag = false; //控制标记 condition_pro.signal();//唤醒对方线程 } finally { lock.unlock(); //释放锁 } } } //生产者线程 class Producer implements Runnable { private Resource res; Producer(Resource res) { this.res = res; } public void run() //复写run方法 { while(true) { try { //调用生产商品方法 res.set("+商品+"); } catch (InterruptedException e) { } } } } //消费者线程 class Consumer implements Runnable { private Resource res; Consumer(Resource res) { this.res = res; } public void run()//复写run方法 { while(true) { try { //调用消费方法 res.out(); } catch (InterruptedException e) { } } } } class ProducerConsumerDemo { public static void main(String[] args) { //创建资源对象 Resource r = new Resource(); //创建生产者线程和消费者线程对象,并把资源对象作为参数传入 Producer pro = new Producer(r); Consumer con = new Consumer(r); //创建两个生产者线程,连个消费者线程,并启用 new Thread(pro).start(); new Thread(pro).start(); new Thread(con).start(); new Thread(con).start(); } }
运行结果
停止线程
JDK1.5版本之前可以用stop方法来停止线程,但是1.5版本更新以后stop方法过时了。那么要如何停止线程呢?
只有一种,run()方法结束。开启多线程运行,运行代码通常是循环结构,只要控制住循环,就可以让run方法结束,也就是线程结束。
如下面的代码:
public voidrun() {
while(flag) {
System.out.println(Thread.currentThread().getName()+"-------run");
}
}
使循环条件为一个变量,等程序运行一段时间后,只要在主函数,或者除了这个线程中的其他线程中
将false赋给标记flag,run方法就会结束,线程也会停止。
但是会出现特殊状况:当线程处于冻结状态,就不会读取到标记,那么线程就不会结束。
当没有指定的方式让冻结恢复到运行状态时,就需要对冻结进行清除,强制让线程恢复到运行状态中来
这样就可以操作标记,让线程结束。Thread类中提供了该方法 interrupt()
特出情况示例
/* 停止线程的特殊情况 线程处于冻结状态,无法读取到标记,所以我们要这么做! */ //创建一个线程,就是等一下要被冻结,唤醒,再结束的线程(怎么那么可怜) class StopThread implements Runnable { //定义标记 private boolean flag =true; //复写run方法 public void run() { //将标记作为循环条件 while(flag) { System.out.println(Thread.currentThread().getName()+"....我运行啦~"); } } //改变标记的方法 public void changeFlag() { flag = false; } } class StopThreadDemo { public static void main(String[] args) { //创建一会儿要停止的线程的对象 StopThread st = new StopThread(); //创建线程,并把一会儿要停止的线程对象作为参数传入 Thread t1 = new Thread(st); Thread t2 = new Thread(st); //线程启动 t1.start(); t2.start(); int num = 0; while(true) { if(num++ == 60) { //当主函数线程运行并且num正好到60的时候,将自定义线程从冻结状态唤醒 t1.interrupt(); t2.interrupt(); //调用改变循环标记的方法,让循环停止 st.changeFlag(); //跳出while循环 break; } //主函数线程输出的语句 System.out.println(Thread.currentThread().getName()+"......."+num); } //结束 System.out.println("over"); } }
运行结果
让主函数线程代码执行完时,冻结的线程执行结束。
什么时候写多线程?
当某些代码需要同时被执行时,就需要用单独的线程进行封装。
多线程扩展知识
1. join():(抢夺CPU执行权)当A线程执行到了B线程的join()方法时,A就会等待,B执行完,A才会执行,join()可以用来临时加入线程执行。2. setPriority():用来设置优先级:
MAX_PRIORITY 最高优先级10
MIN_PRIORITY 最低优先级1
NORM_PRIORITY 分配给线程的默认优先级5
优先级就是抢资源的频率,越大抢到CPU执行权的机会就越大
3. yield():暂停当前正在执行的线程对象,并执行其他线程。临时释放,稍微减缓线程的运行,而且能达到平均运行效果。
谢谢大家的观看~!
-----------android培训、java培训、java学习型技术博客、期待与您交流!---------相关文章推荐
- 黑马程序员-java基础-总结的几点问题
- 面试题3 ----二维数组中的查找
- 蛋糕求职记--小时候淘气,长大了淘宝
- 程序员如何快速准备面试中的算法
- 黑马程序员——Java基础---线程的另一个总结(2)--定时器
- 程序员必须知道的8大排序和3大查找
- 黑马程序员——java基础---关键字、常量、变量、运算符
- 面试题
- 黑马程序员-----IO流
- 黑马程序员——Java基础---线程的另一个总结(1)--基本创建方法
- 面试中的优缺点
- 程序员的学习方法【思考】
- 程序员的学习方法【思考】
- 黑马程序员——Java基础---反射
- Java面试 多线程 生产者消费者
- 黑马程序员--java基础学习笔记2
- 【黑马程序员】Java学习笔记之面向对象高级篇
- [程序猿面试题精选100称号]1.转变为一个二叉搜索树有序双向链表
- 面试题14:调整数组顺序使奇数位于偶数前面
- .net 高频面试题