目录:
- 概述
- 多线程的好处与弊端
- JVM中的多线程解析
- 多线程的创建方式之一:继承Thread类
- 线程的状态
- 多线程创建的方式之二:实现Runnable接口
- 使用方式二创建多线程的好处
- 多线程示例
- 线程安全问题现象
- 线程安全问题产生的原因
- 同步代码块
- 同步的好处与弊端
- 同步的前提
- 同步函数
- 验证同步函数的锁
- 单例模式的线程安全问题的解决方案
- 死锁示例
一、概述 目录
首先得了解进程,打开我们电脑的windows资源管理器,可以直观看到进程的样子:
进程直观上理解就是正在进行的程序。而每个进程包含一个或者多个线程。也就是说一个进程是由若干线程组成的,在程序执行期间,真正执行的是线程,而进程只是负责给该进程中的线程分配执行路径,
所以,线程就是进程中负责程序执行的控制单元(执行路径),一个进程可以有多个执行路径,称为多线程。就像我们再使用QQ给多个好友聊天一样,每一个聊天过程都是一个线程,这些线程都属于QQ这个进程。
而开启多线程就是为了同时运行多部分代码。每一线程都有自己运行的内容,这个内容可以称为线程要执行的任务。
二、多线程的好处与弊端 目录
上一部分说到多线程是为了同时运行多部分代码,但是对于一个cpu而言,在每一个时刻只能执行一个线程,它会在不同线程之间快速切换,由于切换速度很快,所以感觉上去像是多个线程在”同时”执行,现在虽然出现多核技术核数是几乎不可能多过线程数的,所以仍然需要cpu不断在多个线程之间切换,以提高cpu的利用效率。 然而,但是每一个线程都需要一定的内存空间去执行,线程一多,内存空间不足,就会使得电脑显得特别卡,这就是多线程的弊端。注意到cpu在线程之间的切换是随机的。
三、JVM中的多线程解析 目录
JVM启动时就启动了多个线程,至少有两个线程可以分析出来:一个是执行main函数的线程(也称为主线程),另一个是负责垃圾回收的线程。
在JVM垃圾回收方法是finalize方法,该方法由垃圾回收器来调用,而gc() 方法是用来运行垃圾回收器的:
下面展示主线程和垃圾回收线程的运行:
1 package thread.demo; 2 3 class Demo extends Object{ 4 public void finalize(){ 5 System.out.println("demo ok"); 6 } 7 } 8 public class ThreadDemo_1 { 9 10 /** 11 * @param args 12 */ 13 public static void main(String[] args) { 14 // TODO Auto-generated method stub 15 new Demo(); 16 new Demo(); 17 System.gc(); 18 new Demo(); 19 System.out.println("Hello World!"); 20 } 21 22 }
运行结果:
Hello World! demo ok demo ok
可以发现,虽然第19行在第17行代码(垃圾回收)之前,但是第19行代码却先执行,怎么回事呢?因为垃圾回收(第17行)是由垃圾回收线程执行,而第19行代码主线程的部分,cpu从主线程开始执行,然后在主线程和垃圾回收线程之间切换,创建完两个Demo()对象之后,虽然我们调用垃圾回收器,但是垃圾回收程序还没来得及执行,cpu切换到了主线程,于是先打印出了“Hello World!”
于是虽然第19行代码,执行完了,看似整个程序都执行完了,但是JVM(Java 虚拟机)还没有结束,即虽然主线程结束了,但是JVM还要执行垃圾回收线程。
四、多线程创建方式之一:继承Thread类 目录
首先看一看一个简单的打印程序:
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/81178cc93a2a3bb5048d90d76e7ec935.gif)
1 package thread.demo; 2 class Demo_2{ 3 private String name; 4 Demo_2(String name){ 5 this.name = name; 6 } 7 public void show(){ 8 for (int x = -99999999; x < 99999999; x++){}; 9 for (int i = 0; i < 10; i++){ 10 System.out.println(name + "...i" + i); 11 } 12 } 13 } 14 public class ThreadDemo_2 { 15 16 /** 17 * @param args 18 */ 19 public static void main(String[] args) { 20 // TODO Auto-generated method stub 21 Demo_2 d1 = new Demo_2("旺财"); 22 Demo_2 d2 = new Demo_2("xiaoqiang"); 23 d1.show(); 24 d2.show(); 25 } 26 27 }
View Code
运行结果:
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/81178cc93a2a3bb5048d90d76e7ec935.gif)
旺财...i0 旺财...i1 旺财...i2 旺财...i3 旺财...i4 旺财...i5 旺财...i6 旺财...i7 旺财...i8 旺财...i9 xiaoqiang...i0 xiaoqiang...i1 xiaoqiang...i2 xiaoqiang...i3 xiaoqiang...i4 xiaoqiang...i5 xiaoqiang...i6 xiaoqiang...i7 xiaoqiang...i8 xiaoqiang...i9
View Code
这时显示的只是主线程运行的结果,很容易理解!下面通过一种方式,让“旺财”和“xiaoqiang”的打印能够分别运行在不同的线程中,首先查看java的API文档:
翻译:线程 是程序中的执行线程。Java 虚拟机允许应用程序并发地执行多个执行线程。
接着文档中给出了创建线程的方法:
总结出来就是继承Thread类然后重写Thread类中的run()方法,run方法中的代码就是线程要执行的代码,然后调用start方法开启一个线程。
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/81178cc93a2a3bb5048d90d76e7ec935.gif)
1 package thread.demo; 2 class Demo_2 extends Thread { 3 private String name; 4 Demo_2(String name){ 5 this.name = name; 6 } 7 public void run() { 8 show(); 9 } 10 public void show(){ 11 for (int x = -99999999; x < 99999999; x++){}; 12 for (int i = 0; i < 10; i++){ 13 System.out.println(name + "...i" + i); 14 } 15 } 16 } 17 public class ThreadDemo_2 extends Thread { 18 19 /** 20 * @param args 21 */ 22 public static void main(String[] args) { 23 /* 创建线程的目的是为了开启一条执行路径,去运行指定代码和 24 其他代码实现同时运行,而运行的指定代码就是这个执行路径的 25 任务。 JVM创建的主线程的任务都定义在主函数中。 26 而自定义的线程它的任务: 27 Thread类用于描述线程,线程需要任务,所以Thread类也有对 28 任务的描述,这个任务就通过Thread类中的run方法来体现的。 29 也就是说,run方法就是封装自定义线程任务的函数。 30 run方法中定义的就是线程要运行的任务代码!!! 31 32 开启线程是为了运行指定代码,所以只有继承Thread类,并重写run方法。 33 并将运行代码定义在run方法中即可 34 */ 35 Demo_2 d1 = new Demo_2("旺财"); 36 Demo_2 d2 = new Demo_2("xiaoqiang"); 37 d1.start(); 38 d2.start(); 39 } 40 41 }
View Code
运行结果:
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/81178cc93a2a3bb5048d90d76e7ec935.gif)
xiaoqiang...i0 旺财...i0 xiaoqiang...i1 旺财...i1 旺财...i2 旺财...i3 xiaoqiang...i2 xiaoqiang...i3 xiaoqiang...i4 xiaoqiang...i5 xiaoqiang...i6 xiaoqiang...i7 旺财...i4 xiaoqiang...i8 xiaoqiang...i9 旺财...i5 旺财...i6 旺财...i7 旺财...i8 旺财...i9
View Code
可以看出,运行结果正如上面分析的一样,cpu在多个线程之间随机切换,于是打印出的结果与上面只有主线程时结果差别很大。
下面我们让主线程参与进来,即同时看看线程d1,d2和主线程的运行结果:
1 package thread.demo; 2 class Demo_2 extends Thread { 3 private String name; 4 Demo_2(String name){ 5 this.name = name; 6 } 7 public void run() { 8 show(); 9 } 10 public void show(){ 11 for (int x = -99999999; x < 99999999; x++){}; 12 for (int i = 0; i < 10; i++){ 13 System.out.println(name + "...i" + i); 14 } 15 } 16 } 17 public class ThreadDemo_2 extends Thread { 18 19 /** 20 * @param args 21 */ 22 public static void main(String[] args) { 23 /* 创建线程的目的是为了开启一条执行路径,去运行指定代码和 24 其他代码实现同时运行,而运行的指定代码就是这个执行路径的 25 任务。 JVM创建的主线程的任务都定义在主函数中。 26 而自定义的线程它的任务: 27 Thread类用于描述线程,线程需要任务,所以Thread类也有对 28 任务的描述,这个任务就通过Thread类中的run方法来体现的。 29 也就是说,run方法就是封装自定义线程任务的函数。 30 run方法中定义的就是线程要运行的任务代码!!! 31 32 开启线程是为了运行指定代码,所以只有继承Thread类,并重写run方法。 33 并将运行代码定义在run方法中即可 34 */ 35 Demo_2 d1 = new Demo_2("旺财"); 36 Demo_2 d2 = new Demo_2("xiaoqiang"); 37 d1.start(); 38 d2.start(); 39 System.out.println("over"); 40 } 41 42 }
运行结果:
over xiaoqiang...i0 旺财...i0 xiaoqiang...i1 旺财...i1 xiaoqiang...i2 旺财...i2 xiaoqiang...i3 xiaoqiang...i4 旺财...i3 xiaoqiang...i5 旺财...i4 xiaoqiang...i6 旺财...i5 xiaoqiang...i7 旺财...i6 xiaoqiang...i8 旺财...i7 xiaoqiang...i9 旺财...i8 旺财...i9
多次执行,会发现显示结果一直变化,这就是多个线程随机占用cpu的结果。 当然,如果想看到到底是哪个线程正在执行,可以调用Thread中的currentThread().getName()方法,其中currentThread()是用来返回当前的线程对象,然后用线程对象继续调用getName()就是返回当前线程的名字。程序如下:
1 package thread.demo; 2 class Demo_2 extends Thread { 3 private String name; 4 Demo_2(String name){ 5 this.name = name; 6 } 7 public void run() { 8 show(); 9 } 10 public void show(){ 11 for (int x = -99999999; x < 99999999; x++){}; 12 for (int i = 0; i < 10; i++){ 13 System.out.println(name + "...i" + "...name: " + getName()); 14 } 15 } 16 } 17 public class ThreadDemo_2 extends Thread { 18 19 /** 20 * @param args 21 */ 22 public static void main(String[] args) { 23 /* 创建线程的目的是为了开启一条执行路径,去运行指定代码和 24 其他代码实现同时运行,而运行的指定代码就是这个执行路径的 25 任务。 JVM创建的主线程的任务都定义在主函数中。 26 而自定义的线程它的任务: 27 Thread类用于描述线程,线程需要任务,所以Thread类也有对 28 任务的描述,这个任务就通过Thread类中的run方法来体现的。 29 也就是说,run方法就是封装自定义线程任务的函数。 30 run方法中定义的就是线程要运行的任务代码!!! 31 32 开启线程是为了运行指定代码,所以只有继承Thread类,并重写run方法。 33 并将运行代码定义在run方法中即可 34 */ 35 Demo_2 d1 = new Demo_2("旺财"); 36 Demo_2 d2 = new Demo_2("xiaoqiang"); 37 d1.start(); 38 d2.start(); 39 System.out.println("over" + "..." + Thread.currentThread().getName()); 40 } 41 42 }
运行结果:
over...main 旺财...i...name: Thread-0 旺财...i...name: Thread-0 旺财...i...name: Thread-0 旺财...i...name: Thread-0 旺财...i...name: Thread-0 旺财...i...name: Thread-0 xiaoqiang...i...name: Thread-1 xiaoqiang...i...name: Thread-1 xiaoqiang...i...name: Thread-1 xiaoqiang...i...name: Thread-1 xiaoqiang...i...name: Thread-1 xiaoqiang...i...name: Thread-1 xiaoqiang...i...name: Thread-1 xiaoqiang...i...name: Thread-1 xiaoqiang...i...name: Thread-1 xiaoqiang...i...name: Thread-1 旺财...i...name: Thread-0 旺财...i...name: Thread-0 旺财...i...name: Thread-0 旺财...i...name: Thread-0
注意主线程的名字是固定的就是main,然后自定义的线程从0开始编号。
五、线程的状态 目录
六、多线程创建的方式之二:实现Runnable接口 目录
首先来读一下API文档关于Runnable的描述:
划红线部分:Runnable接口由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run的无参数方法。于是采用实现接口的方式来实现多线程:
- 定义Runnable接口
- 覆盖Runnable接口中的run方法,将线程任务代码封装到run方法中。
- 通过Thread类创建对线,并将Runnable接口中的子类对象作为参数传递给Thread类的构造函数。因为线程任务都封装在Runnable接口子类对象的run方法中,所以在创建时必须要明确要运行的任务,如果不传递这个对象,Thread类会运行它自己的run方法,而不是我们定义在接口中的方法。
- 调用线程的start方法开启线程。
代码如下:
1 package thread.demo; 2 //通过实现接口的方式实现多线程创建 3 class Demo_3 implements Runnable { 4 public void run() { 5 show(); 6 } 7 public void show(){ 8 for (int x = -99999999; x < 99999999; x++){}; 9 for (int i = 0; i < 10; i++){ 10 System.out.println(Thread.currentThread().getName()); 11 } 12 } 13 } 14 15 public class ThreadDemo_3 extends Thread { 16 17 /** 18 * @param args 19 */ 20 public static void main(String[] args) { 21 //创建一个Runnable接口的子类对象 22 Demo_3 d = new Demo_3(); 23 //将上述Runnable接口的子类对象传入Thread构造函数, 24 //创建线程 25 Thread t1 = new Thread(d); 26 Thread t2 = new Thread(d); 27 t1.start(); 28 t2.start(); 29 } 30 31 }
运行结果:
Thread-1 Thread-0 Thread-1 Thread-0 Thread-1 Thread-0 Thread-0 Thread-1 Thread-0 Thread-1 Thread-0 Thread-1 Thread-0 Thread-1 Thread-0 Thread-1 Thread-1 Thread-1 Thread-0 Thread-0
同样可以看见两个线程在随机切换执行。
细节:通过阅读API文档,可以发现,Thread类里面定义了自己的run方法,而Runnable也有run方法,而Thread的构造方法包含着下面两种(当然不止两种):
第一种构造方法不包含任何参数,那么在使用Thread创建的线程对象在运行时,就调用Thread类自己的run方法,如果传入一个Runnable子类对象,那么在使用Thread类创建对象时,运行的任务就是Runnable接口中定义run方法。原理用代码简单解释如下:
1 package thread.demo; 2 3 class Thread { 4 private Runnable r; 5 Thread() {} 6 Thread(Runnable) { 7 this.r = r; 8 } 9 10 public void run() { 11 r.run(); 12 } 13 14 public void start() { 15 run(); 16 } 17 } 18 19 public class SubThread extends Thread { 20 21 public void run() { 22 System.out.println("dsa"); 23 } 24 25 }
在Thread内部有一个私有的Runable子类对象,可以看出,当我们把Runable子类 r 对象传递给Thread类构造函数的时候,启动start()就会调用run(),而run()接着调用 r 的run方法;
但是当我们直接通过上面介绍的方式一,即直接继承Thread类创建线程的时候,如19-23行所示,我们需要覆盖Thread类中的run方法,那么SubThread类的对象就在通过start方法启动线程的时候调用的run方法时就会调用我们在Thread子类中自己定义的run方法(21-22行)!
七、使用方式二创建多线程的好处 目录
创建线程的两种方式:
- 方式一:继承Thread
- 方式二:实现Runnable接口
好处:
1)将线程的任务从线程的子类中分离出来,进行了单独封装,按照面向对象的思想将任务封装成对象。
2)避免了java单继承的局限性
所以,创建线程第二种方式较为常用!!!
八、多线程示例 目录
下面实现四个售票员(四个线程)一起卖100张票的示例。
1 package thread.demo; 2 //买票:四个售票员一起卖100张票 3 class Ticket implements Runnable { 4 private int num = 100; 5 public void run() { 6 while(true) { 7 if (num > 0) { 8 System.out.println(Thread.currentThread().getName() + "...sale..." + num--); 9 } 10 } 11 } 12 } 13 14 public class TicketDemo { 15 16 17 public static void main(String[] args) { 18 19 Ticket t = new Ticket(); 20 /* 21 Ticket t1 = new Ticket(); 22 Ticket t2 = new Ticket(); 23 Ticket t3 = new Ticket(); 24 Ticket t4 = new Ticket(); 25 */ 26 Thread seller1 = new Thread(t); 27 Thread seller2 = new Thread(t); 28 Thread seller3 = new Thread(t); 29 Thread seller4 = new Thread(t); 30 31 seller1.start(); 32 seller2.start(); 33 seller3.start(); 34 seller4.start(); 35 } 36 37 }
运行结果:
Thread-0...sale...100 Thread-3...sale...97 Thread-2...sale...98 Thread-1...sale...99 Thread-2...sale...94 Thread-3...sale...95 Thread-0...sale...96 Thread-3...sale...91 Thread-2...sale...92 Thread-2...sale...88 Thread-2...sale...87 Thread-2...sale...86 Thread-1...sale...93 Thread-1...sale...84 Thread-1...sale...83 Thread-2...sale...85 Thread-3...sale...89 Thread-0...sale...90 Thread-3...sale...80 Thread-2...sale...81 Thread-1...sale...82 Thread-1...sale...76 Thread-1...sale...75 Thread-2...sale...77 Thread-3...sale...78 Thread-0...sale...79 Thread-3...sale...72 Thread-2...sale...73 Thread-1...sale...74 Thread-2...sale...69 Thread-3...sale...70 Thread-0...sale...71 Thread-3...sale...66 Thread-2...sale...67 Thread-1...sale...68 Thread-2...sale...63 Thread-2...sale...61 Thread-3...sale...64 Thread-0...sale...65 Thread-3...sale...59 Thread-2...sale...60 Thread-1...sale...62 Thread-2...sale...56 Thread-3...sale...57 Thread-0...sale...58 Thread-3...sale...53 Thread-2...sale...54 Thread-1...sale...55 Thread-1...sale...49 Thread-2...sale...50 Thread-3...sale...51 Thread-0...sale...52 Thread-0...sale...45 Thread-3...sale...46 Thread-2...sale...47 Thread-1...sale...48 Thread-2...sale...42 Thread-3...sale...43 Thread-0...sale...44 Thread-3...sale...39 Thread-2...sale...40 Thread-1...sale...41 Thread-2...sale...36 Thread-3...sale...37 Thread-0...sale...38 Thread-3...sale...33 Thread-2...sale...34 Thread-1...sale...35 Thread-2...sale...30 Thread-3...sale...31 Thread-0...sale...32 Thread-3...sale...27 Thread-2...sale...28 Thread-1...sale...29 Thread-2...sale...24 Thread-3...sale...25 Thread-0...sale...26 Thread-3...sale...21 Thread-2...sale...22 Thread-1...sale...23 Thread-2...sale...18 Thread-3...sale...19 Thread-0...sale...20 Thread-3...sale...15 Thread-2...sale...16 Thread-1...sale...17 Thread-2...sale...12 Thread-3...sale...13 Thread-0...sale...14 Thread-3...sale...9 Thread-2...sale...10 Thread-1...sale...11 Thread-2...sale...6 Thread-3...sale...7 Thread-0...sale...8 Thread-3...sale...3 Thread-2...sale...4 Thread-1...sale...5 Thread-3...sale...1 Thread-0...sale...2
看到四个线程一起把100张票卖完了.
九、线程安全问题现象 目录
分析上面的示例,由于线程之间随机切换执行,假如售票员0(线程0),卖到了1号票,此时 num = 1 ;
if (num > 0) { System.out.println(Thread.currentThread().getName() + "...sale..." + num--); }
经过判断,满足if 条件,进入后面的代码块,但是存在一种情况就是,线程0还没有来得及执行打印语句,就切换到了线程1,此时线程0处于等待状态(等待继续执行…), 此时num仍然为1, 然后线程1判断
满足条件,顺利执行完,之后num–, 于是num = 0; 恰好随机切换到线程0, 然后线程0执行打印语句(Thread-0…sale…0),就是说售票员0 把0号票卖出去了,显然不可以!这种情况就导致了线程不安全!
为了使得程序出现我们分析的这种不安全的情况,需要在示例代码在第7行之后稍微停顿一下,然后执行后面的打印语句,如果不停顿,现在cpu的计算速度很快,判断 if (num > 0)为真之后往往很快就会执行到打印语句,上面分析的情况很难观测到,于是为了说明问题,做如下添加:
1 package thread.demo; 2 //买票:四个售票员一起卖100张票 3 class Ticket implements Runnable { 4 private int num = 100; 5 public void run() { 6 while(true) { 7 if (num > 0) { 8 // 让线程sleep一会,好让打印语句还没来得及执行,其他线程 9 // 就切换进来,这样方便我们观测线程安全隐患 10 try { 11 Thread.sleep(20); 12 } catch (InterruptedException e) { 13 // TODO Auto-generated catch block 14 e.printStackTrace(); 15 } 16 17 System.out.println(Thread.currentThread().getName() + "...sale..." + num--); 18 } 19 } 20 } 21 } 22 23 public class TicketDemo { 24 25 26 public static void main(String[] args) { 27 28 Ticket t = new Ticket(); 29 /* 30 Ticket t1 = new Ticket(); 31 Ticket t2 = new Ticket(); 32 Ticket t3 = new Ticket(); 33 Ticket t4 = new Ticket(); 34 */ 35 Thread seller1 = new Thread(t); 36 Thread seller2 = new Thread(t); 37 Thread seller3 = new Thread(t); 38 Thread seller4 = new Thread(t); 39 40 seller1.start(); 41 seller2.start(); 42 seller3.start(); 43 seller4.start(); 44 } 45 46 }
运行结果:
Thread-3...sale...100 Thread-2...sale...98 Thread-1...sale...100 Thread-0...sale...99 Thread-0...sale...95 Thread-3...sale...97 Thread-2...sale...97 Thread-1...sale...96 Thread-2...sale...94 Thread-3...sale...94 Thread-1...sale...94 Thread-0...sale...94 Thread-0...sale...93 Thread-3...sale...91 Thread-1...sale...92 Thread-2...sale...93 Thread-3...sale...88 Thread-0...sale...89 Thread-2...sale...89 Thread-1...sale...90 Thread-0...sale...87 Thread-2...sale...84 Thread-3...sale...85 Thread-1...sale...86 Thread-2...sale...83 Thread-3...sale...82 Thread-1...sale...82 Thread-0...sale...83 Thread-0...sale...81 Thread-3...sale...80 Thread-1...sale...80 Thread-2...sale...81 Thread-3...sale...78 Thread-0...sale...79 Thread-1...sale...79 Thread-2...sale...77 Thread-2...sale...76 Thread-1...sale...75 Thread-3...sale...75 Thread-0...sale...76 Thread-2...sale...74 Thread-1...sale...73 Thread-3...sale...73 Thread-0...sale...74 Thread-0...sale...72 Thread-1...sale...70 Thread-3...sale...71 Thread-2...sale...69 Thread-0...sale...68 Thread-2...sale...65 Thread-1...sale...67 Thread-3...sale...66 Thread-1...sale...64 Thread-2...sale...61 Thread-0...sale...62 Thread-3...sale...63 Thread-2...sale...59 Thread-1...sale...57 Thread-3...sale...60 Thread-0...sale...58 Thread-2...sale...56 Thread-1...sale...55 Thread-3...sale...54 Thread-0...sale...53 Thread-1...sale...52 Thread-2...sale...51 Thread-0...sale...50 Thread-3...sale...49 Thread-2...sale...48 Thread-1...sale...47 Thread-3...sale...46 Thread-0...sale...45 Thread-1...sale...44 Thread-3...sale...42 Thread-0...sale...43 Thread-2...sale...41 Thread-1...sale...39 Thread-0...sale...38 Thread-3...sale...40 Thread-2...sale...37 Thread-2...sale...36 Thread-0...sale...34 Thread-3...sale...35 Thread-1...sale...33 Thread-1...sale...32 Thread-2...sale...31 Thread-3...sale...30 Thread-0...sale...29 Thread-0...sale...28 Thread-3...sale...27 Thread-1...sale...26 Thread-2...sale...25 Thread-1...sale...24 Thread-2...sale...23 Thread-0...sale...22 Thread-3...sale...21 Thread-3...sale...20 Thread-1...sale...18 Thread-2...sale...19 Thread-0...sale...20 Thread-2...sale...17 Thread-3...sale...15 Thread-0...sale...16 Thread-1...sale...14 Thread-3...sale...13 Thread-0...sale...12 Thread-2...sale...11 Thread-1...sale...10 Thread-0...sale...9 Thread-3...sale...8 Thread-1...sale...7 Thread-2...sale...6 Thread-3...sale...5 Thread-0...sale...4 Thread-2...sale...3 Thread-1...sale...2 Thread-3...sale...1 Thread-0...sale...0 Thread-2...sale...-1 Thread-1...sale...-2
可以看到售票员竟然卖出了0,-1,-2号票!!这就是线程安全问题的简单演示,每次运行出现的结果不一样,也有可能恰好正常,没有出现安全问题,以为线程切换的不确定性,但是这种问题一旦出现,通常很致命,所以必须注意!
十、线程安全问题产生的原因 目录
由上面可以知道,线程安全问题必须要解决,但是解决问题关键是分析原因!根据上面示例总结出以下原因:
- 多个线程操作共享的数据
- 操作共享数据的线程代码有多条
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。
十一、同步代码块 目录
既然知道产生线程安全问题的原因,就开始着手解决。
解决思路:
将多条操作操作共享数据的代码封装起来, 当有线程在执行这些代码的时候,其他线程是不可以参与运算。
必须要当前线程把这些代码都执行完毕以后,其他线程才可以参与运算.
Java中,用同步代码块就可以解决这个问题。同步代码块的格式:
synchronized(对象){
需要被同步的代码;
}
其中的“对象”可以看做一个标记,可以使用任何类型的对象,包括自定义的对象。当然,为了方便,使用Object类的对象即可。
1 package thread.demo; 2 //买票:四个售票员一起卖100张票 3 class Ticket implements Runnable { 4 private int num = 100; 5 Object obj = new Object(); 6 public void run() { 7 while(true) { 8 synchronized(obj) { 9 if (num > 0) { 10 // 让线程sleep一会,好让打印语句还没来得及执行后面的打印语句,其他线程 11 // 就切换进来,这样方便我们观测线程安全隐患 12 try { 13 Thread.sleep(20); 14 } catch (InterruptedException e) { 15 // TODO Auto-generated catch block 16 e.printStackTrace(); 17 } 18 19 System.out.println(Thread.currentThread().getName() + "...sale..." + num--); 20 } 21 } 22 } 23 } 24 } 25 26 public class TicketDemo { 27 28 29 public static void main(String[] args) { 30 31 Ticket t = new Ticket(); 32 /* 33 Ticket t1 = new Ticket(); 34 Ticket t2 = new Ticket(); 35 Ticket t3 = new Ticket(); 36 Ticket t4 = new Ticket(); 37 */ 38 Thread seller1 = new Thread(t); 39 Thread seller2 = new Thread(t); 40 Thread seller3 = new Thread(t); 41 Thread seller4 = new Thread(t); 42 43 seller1.start(); 44 seller2.start(); 45 seller3.start(); 46 seller4.start(); 47 } 48 49 }
运行结果:
Thread-0...sale...100 Thread-0...sale...99 Thread-0...sale...98 Thread-0...sale...97 Thread-0...sale...96 Thread-0...sale...95 Thread-0...sale...94 Thread-0...sale...93 Thread-0...sale...92 Thread-0...sale...91 Thread-0...sale...90 Thread-0...sale...89 Thread-0...sale...88 Thread-2...sale...87 Thread-2...sale...86 Thread-2...sale...85 Thread-3...sale...84 Thread-3...sale...83 Thread-3...sale...82 Thread-3...sale...81 Thread-1...sale...80 Thread-1...sale...79 Thread-1...sale...78 Thread-1...sale...77 Thread-1...sale...76 Thread-1...sale...75 Thread-3...sale...74 Thread-2...sale...73 Thread-2...sale...72 Thread-2...sale...71 Thread-2...sale...70 Thread-2...sale...69 Thread-2...sale...68 Thread-0...sale...67 Thread-0...sale...66 Thread-0...sale...65 Thread-0...sale...64 Thread-0...sale...63 Thread-0...sale...62 Thread-2...sale...61 Thread-2...sale...60 Thread-3...sale...59 Thread-3...sale...58 Thread-3...sale...57 Thread-3...sale...56 Thread-3...sale...55 Thread-3...sale...54 Thread-3...sale...53 Thread-3...sale...52 Thread-3...sale...51 Thread-3...sale...50 Thread-3...sale...49 Thread-3...sale...48 Thread-3...sale...47 Thread-3...sale...46 Thread-3...sale...45 Thread-3...sale...44 Thread-3...sale...43 Thread-3...sale...42 Thread-3...sale...41 Thread-1...sale...40 Thread-1...sale...39 Thread-1...sale...38 Thread-1...sale...37 Thread-1...sale...36 Thread-1...sale...35 Thread-1...sale...34 Thread-1...sale...33 Thread-1...sale...32 Thread-1...sale...31 Thread-1...sale...30 Thread-1...sale...29 Thread-1...sale...28 Thread-1...sale...27 Thread-1...sale...26 Thread-1...sale...25 Thread-3...sale...24 Thread-2...sale...23 Thread-2...sale...22 Thread-2...sale...21 Thread-0...sale...20 Thread-0...sale...19 Thread-0...sale...18 Thread-0...sale...17 Thread-2...sale...16 Thread-2...sale...15 Thread-2...sale...14 Thread-2...sale...13 Thread-3...sale...12 Thread-3...sale...11 Thread-3...sale...10 Thread-3...sale...9 Thread-3...sale...8 Thread-3...sale...7 Thread-1...sale...6 Thread-1...sale...5 Thread-1...sale...4 Thread-1...sale...3 Thread-3...sale...2 Thread-3...sale...1
可以看出,问题得到了很好地解决!
十二、同步的好处与弊端 目录
首先讨论下,同步到底是如何实现的。
假如0线程执行到run方法的同步代码块,那么 0 线程就持有了 obj, 即obj 被加载到了0线程里面,当其他线程过来时,它们也需要加载obj才能进入同步代码块,但是在线程没有执行完同步代码块之前,obj一直被0线程占有,所以其他线程无法进入同步代码块,知道0线程执行完同步代码块释放 obj,其他线程才有机会加载obj, 然后进入同步代码块。所以obj就像一把锁一样,里面的不出来,外面的就进不去。所以常常称为同步锁!谁拿到了锁,谁就进得去,出来之后释放锁,以便其他线程有机会使用。
同步的好处:解决了线程安全问题。
同步的弊端:相对降低了效率,因为同步外的线程都会去判断同步锁。
十三、同步的前提 目录
同步中,必须有多个线程并使用同一个锁,因为一个线程没必要同步,数据全被这一个线程使用,就不存在所谓的线程安全问题。而如果不使用同一个锁,即使一个线程在执行加了锁的代码块,其他线程同样可以通过拿到其他锁进来参与运算。
十四、同步函数 目录
一个稍微简单点的多线程程序示例:
1 package thread.demo; 2 /* 3 需求:两个储户,每个都到银行存钱,每次存100,共存3次 4 */ 5 class Bank { 6 private int sum; 7 public void add(int num) { 8 sum += num; 9 System.out.println("sum = " + sum); 10 } 11 } 12 13 class Customer implements Runnable { 14 Bank bank = new Bank(); 15 public void run() { 16 for (int x = 0; x < 3; x++) { 17 bank.add(100); 18 } 19 } 20 } 21 22 public class BankDemo { 23 24 /** 25 * @param args 26 */ 27 public static void main(String[] args) { 28 // TODO Auto-generated method stub 29 Customer customer = new Customer(); 30 Thread t1 = new Thread(customer); 31 Thread t2 = new Thread(customer); 32 33 t1.start(); 34 t2.start(); 35 36 } 37 38 }
运行结果:
sum = 100 sum = 200 sum = 300 sum = 500 sum = 400 sum = 600
当然,由于线程的随机切换,显示结果有点乱,最终两人共向银行存入了600.
分析一下这个程序:run方法里面的代码调用了add方法,sum 是两个线程的共享数据,对于共享数据的操作不止一条:
sum += num; System.out.println("sum = " + sum);
假如第一个用户(线程0)存入100,执行第一条语句,sum 就变为100. 正常接下来就应该输出 sum = 100, 但是这时存在一种情况:线程0 还没来得及输出,第二个用户(线程1)存入100,于是sum = 100 + 100 = 200, 线程1 然后正常输出 sum = 200,刚输出完成,线程0又切入了,接着执行打印语句,本来他存入的只是100,但是由于sum = 200, 线程0 也打印出sum = 200, 对应这实际情况就是,用户1存入了100,系统却显示存入了200,显然存在着线程安全问题。同样,为了展示这个安全问题,在上面两条语句之间假如一定的停顿:
sum += num; try { Thread.sleep(20); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("sum = " + sum);
运行结果:
sum = 200 sum = 200 sum = 400 sum = 400 sum = 600 sum = 600
可见,200显示了两次,肯定是有一个用户存入100时,却打印出了200. 显然不合理!
首先想到的就是同步:
1 class Bank { 2 private int sum; 3 Object obj = new Object(); 4 public void add(int num) { 5 synchronized(obj) { 6 sum += num; 7 try { 8 Thread.sleep(20); 9 } catch (InterruptedException e) { 10 // TODO Auto-generated catch block 11 e.printStackTrace(); 12 } 13 System.out.println("sum = " + sum); 14 } 15 } 16 }
运行结果:
sum = 100 sum = 200 sum = 300 sum = 400 sum = 500 sum = 600
问题解决!
但是,发现,要创建对象,写synchronized代码块是不是有点麻烦呢???我们发现add函数里面的内容都需要同步,就是说add函数里面的代码就是我们要同步的代码,于是直接同步这个函数就可以了,即同步函数:
1 class Bank { 2 private int sum; 3 //Object obj = new Object(); 4 public synchronized void add(int num) {//同步函数 5 //synchronized(obj) { 6 sum += num; 7 try { 8 Thread.sleep(20); 9 } catch (InterruptedException e) { 10 // TODO Auto-generated catch block 11 e.printStackTrace(); 12 } 13 System.out.println("sum = " + sum); 14 //} 15 } 16 }
运行结果同上!
十五、同步函数锁的验证 目录
由前面的分析知道,同步是需要一把“锁”,而锁可以是任意类型的对象,那么同步函数到底使用的是什么锁呢??下面来验证。
回到前面售票的程序,改成两个售票员,一个售票员卖票线程放在同步代码块里面,另一个售票员的的线程放在同步函数里面,如果这两个线程用的是同一个锁,那么就不会出现安全隐患。
于是在程序改为下面:
1 package thread.demo; 2 //买票:四个售票员一起卖100张票 3 class Ticket_2 implements Runnable { 4 private int num = 100; 5 Object obj = new Object(); 6 boolean flag = true; 7 public void run() { 8 if (flag) 9 { 10 while (true) 11 { 12 synchronized(obj) 13 { 14 if (num > 0) 15 { 16 // 让线程sleep一会,好让打印语句还没来得及执行后面的打印语句,其他线程 17 // 就切换进来,这样方便我们观测线程安全隐患 18 try { 19 Thread.sleep(20); 20 } catch (InterruptedException e) { 21 // TODO Auto-generated catch block 22 e.printStackTrace(); 23 } 24 System.out.println(Thread.currentThread().getName() + "...bloc k..." + num--); 25 } 26 } 27 } 28 29 }//end if 30 else 31 while (true) show(); 32 } 33 34 public synchronized void show() 35 { 36 if (num > 0) 37 { 38 // 让线程sleep一会,好让打印语句还没来得及执行,其他线程 39 // 就切换进来,这样方便我们观测线程安全隐患 40 try { 41 Thread.sleep(20); 42 } catch (InterruptedException e) { 43 // TODO Auto-generated catch block 44 e.printStackTrace(); 45 } 46 System.out.println(Thread.currentThread().getName() + "...function..." + num--); 47 } 48 } 49 } 50 51 public class SynFunctionLockDemo 52 { 53 54 55 public static void main(String[] args) 56 { 57 58 Ticket_2 t = new Ticket_2(); 59 60 Thread seller1 = new Thread(t); 61 Thread seller2 = new Thread(t); 62 63 64 seller1.start(); //在同步代码块执行 65 t.flag = false; // 标志变为false,使得下一个线程在同步函数执行 66 seller2.start(); 67 68 } 69 }
如果是在同步函数里面执行的代码块会打印出带有“function”的字符串。如果是在同步代码块里面执行的会打印出带有”bloc k”的字符串,执行如下:
Thread-0...function...100 Thread-0...function...99 Thread-0...function...98 Thread-0...function...97 Thread-0...function...96 Thread-0...function...95 Thread-0...function...94 Thread-0...function...93 Thread-0...function...92 Thread-0...function...91 Thread-0...function...90 Thread-0...function...89 Thread-0...function...88 Thread-0...function...87 Thread-0...function...86 Thread-0...function...85 Thread-1...function...84 Thread-1...function...83 Thread-1...function...82 Thread-1...function...81 Thread-0...function...80 Thread-0...function...79 Thread-0...function...78 Thread-0...function...77 Thread-0...function...76 Thread-0...function...75 Thread-0...function...74 Thread-1...function...73 Thread-1...function...72 Thread-0...function...71 Thread-0...function...70 Thread-0...function...69 Thread-1...function...68 Thread-1...function...67 Thread-1...function...66 Thread-0...function...65 Thread-0...function...64 Thread-0...function...63 Thread-0...function...62 Thread-1...function...61 Thread-1...function...60 Thread-1...function...59 Thread-1...function...58 Thread-0...function...57 Thread-0...function...56 Thread-0...function...55 Thread-1...function...54 Thread-1...function...53 Thread-1...function...52 Thread-1...function...51 Thread-1...function...50 Thread-1...function...49 Thread-1...function...48 Thread-1...function...47 Thread-0...function...46 Thread-0...function...45 Thread-0...function...44 Thread-0...function...43 Thread-1...function...42 Thread-1...function...41 Thread-1...function...40 Thread-1...function...39 Thread-1...function...38 Thread-1...function...37 Thread-1...function...36 Thread-1...function...35 Thread-1...function...34 Thread-1...function...33 Thread-1...function...32 Thread-1...function...31 Thread-1...function...30 Thread-1...function...29 Thread-1...function...28 Thread-1...function...27 Thread-0...function...26 Thread-0...function...25 Thread-0...function...24 Thread-1...function...23 Thread-1...function...22 Thread-1...function...21 Thread-1...function...20 Thread-1...function...19 Thread-1...function...18 Thread-1...function...17 Thread-1...function...16 Thread-0...function...15 Thread-0...function...14 Thread-0...function...13 Thread-1...function...12 Thread-1...function...11 Thread-1...function...10 Thread-1...function...9 Thread-1...function...8 Thread-1...function...7 Thread-0...function...6 Thread-0...function...5 Thread-1...function...4 Thread-1...function...3 Thread-1...function...2 Thread-1...function...1
发现竟然都在同步函数里面执行,什么原因呢?原因在于:主线程开启以后一口气运行到了66行代码,注意这里虽然线程0和线程1被开启,即具有了执行资格,但是还不具备执行权,执行权仍然被主线程占有着,等待主线程完事之后,线程0和线程1在真正抢到执行权的时候,发现flag = false,于是全部走进了同步函数!!! 要想使得线程0和线程1分别走进不同的同步方法,需要在开启线程0之后,让主线程停顿一会,让线程0开始执行(此时线程1还没有被开启,是不可能执行的),进入同步代码块,然后等到主线程再执行的时候就会把flag置为false,开启线程1,线程1就可以进入同步函数。
具体改动如下:
1 public static void main(String[] args) 2 { 3 4 Ticket_2 t = new Ticket_2(); 5 6 Thread seller1 = new Thread(t); 7 Thread seller2 = new Thread(t); 8 9 10 seller1.start(); //在同步代码块执行 11 12 // 主线程停顿一会,给线程0执行的机会!! 13 try { 14 Thread.sleep(20); 15 } catch (InterruptedException e) { 16 // TODO Auto-generated catch block 17 e.printStackTrace(); 18 } 19 20 t.flag = false; // 标志变为false,使得下一个线程在同步函数执行 21 seller2.start(); 22 23 }
运行结果:
Thread-0...bloc k...100 Thread-0...bloc k...99 Thread-1...function...98 Thread-0...bloc k...96 Thread-1...function...97 Thread-0...bloc k...94 Thread-1...function...95 Thread-0...bloc k...93 Thread-1...function...92 Thread-0...bloc k...91 Thread-1...function...90 Thread-1...function...89 Thread-0...bloc k...89 Thread-0...bloc k...88 Thread-1...function...88 Thread-0...bloc k...87 Thread-1...function...87 Thread-1...function...86 Thread-0...bloc k...86 Thread-1...function...84 Thread-0...bloc k...85 Thread-1...function...82 Thread-0...bloc k...83 Thread-0...bloc k...80 Thread-1...function...81 Thread-0...bloc k...79 Thread-1...function...78 Thread-0...bloc k...77 Thread-1...function...76 Thread-1...function...75 Thread-0...bloc k...74 Thread-1...function...73 Thread-0...bloc k...72 Thread-1...function...71 Thread-0...bloc k...70 Thread-1...function...69 Thread-0...bloc k...68 Thread-1...function...67 Thread-0...bloc k...66 Thread-1...function...65 Thread-0...bloc k...64 Thread-1...function...63 Thread-0...bloc k...62 Thread-1...function...61 Thread-0...bloc k...60 Thread-1...function...59 Thread-0...bloc k...58 Thread-1...function...57 Thread-0...bloc k...56 Thread-1...function...55 Thread-0...bloc k...54 Thread-1...function...53 Thread-0...bloc k...52 Thread-1...function...51 Thread-0...bloc k...50 Thread-1...function...49 Thread-0...bloc k...48 Thread-1...function...47 Thread-0...bloc k...46 Thread-1...function...45 Thread-0...bloc k...44 Thread-1...function...43 Thread-0...bloc k...42 Thread-1...function...41 Thread-0...bloc k...40 Thread-1...function...39 Thread-0...bloc k...38 Thread-1...function...37 Thread-0...bloc k...36 Thread-1...function...35 Thread-0...bloc k...34 Thread-1...function...33 Thread-0...bloc k...32 Thread-1...function...31 Thread-0...bloc k...30 Thread-1...function...29 Thread-0...bloc k...28 Thread-1...function...27 Thread-0...bloc k...26 Thread-1...function...25 Thread-0...bloc k...24 Thread-1...function...23 Thread-0...bloc k...22 Thread-1...function...21 Thread-0...bloc k...20 Thread-1...function...19 Thread-0...bloc k...18 Thread-0...bloc k...17 Thread-1...function...16 Thread-1...function...15 Thread-0...bloc k...14 Thread-1...function...13 Thread-0...bloc k...12 Thread-1...function...11 Thread-0...bloc k...10 Thread-1...function...9 Thread-0...bloc k...8 Thread-1...function...7 Thread-0...bloc k...6 Thread-1...function...5 Thread-0...bloc k...4 Thread-1...function...3 Thread-0...bloc k...2 Thread-1...function...1 Thread-0...bloc k...0
可以看到,线程用到了不同的同步方式:同步函数(1线程)和同步代码块(0线程)!但是,也看到89,88,87,86号票被出售了两次,而且还售出了0号票(每次执行情况会不同,但是都会出现类似的线程问题),这说明线程0和线程1没有同步,即二者目前使用的锁不一致,即同步函数使用的不是Object 类型的锁!
我们知道类里面的函数都默认持有this代表着调用该类的对象,同步函数当让持有this啦,试着把同步代码块的锁改为this:
synchronized(this)
然后多次执行,结果如下:
Thread-0...bloc k...100 Thread-0...bloc k...99 Thread-0...bloc k...98 Thread-0...bloc k...97 Thread-0...bloc k...96 Thread-0...bloc k...95 Thread-0...bloc k...94 Thread-0...bloc k...93 Thread-0...bloc k...92 Thread-0...bloc k...91 Thread-0...bloc k...90 Thread-0...bloc k...89 Thread-0...bloc k...88 Thread-0...bloc k...87 Thread-0...bloc k...86 Thread-1...function...85 Thread-1...function...84 Thread-1...function...83 Thread-0...bloc k...82 Thread-0...bloc k...81 Thread-0...bloc k...80 Thread-0...bloc k...79 Thread-1...function...78 Thread-1...function...77 Thread-1...function...76 Thread-0...bloc k...75 Thread-0...bloc k...74 Thread-0...bloc k...73 Thread-0...bloc k...72 Thread-0...bloc k...71 Thread-1...function...70 Thread-1...function...69 Thread-0...bloc k...68 Thread-0...bloc k...67 Thread-0...bloc k...66 Thread-1...function...65 Thread-1...function...64 Thread-1...function...63 Thread-1...function...62 Thread-1...function...61 Thread-0...bloc k...60 Thread-0...bloc k...59 Thread-1...function...58 Thread-1...function...57 Thread-1...function...56 Thread-1...function...55 Thread-0...bloc k...54 Thread-0...bloc k...53 Thread-0...bloc k...52 Thread-0...bloc k...51 Thread-1...function...50 Thread-1...function...49 Thread-1...function...48 Thread-0...bloc k...47 Thread-0...bloc k...46 Thread-0...bloc k...45 Thread-1...function...44 Thread-1...function...43 Thread-1...function...42 Thread-0...bloc k...41 Thread-0...bloc k...40 Thread-0...bloc k...39 Thread-0...bloc k...38 Thread-0...bloc k...37 Thread-0...bloc k...36 Thread-0...bloc k...35 Thread-0...bloc k...34 Thread-1...function...33 Thread-1...function...32 Thread-1...function...31 Thread-1...function...30 Thread-1...function...29 Thread-1...function...28 Thread-1...function...27 Thread-0...bloc k...26 Thread-0...bloc k...25 Thread-0...bloc k...24 Thread-0...bloc k...23 Thread-0...bloc k...22 Thread-1...function...21 Thread-0...bloc k...20 Thread-0...bloc k...19 Thread-0...bloc k...18 Thread-0...bloc k...17 Thread-1...function...16 Thread-1...function...15 Thread-1...function...14 Thread-0...bloc k...13 Thread-0...bloc k...12 Thread-0...bloc k...11 Thread-0...bloc k...10 Thread-1...function...9 Thread-1...function...8 Thread-1...function...7 Thread-0...bloc k...6 Thread-0...bloc k...5 Thread-0...bloc k...4 Thread-0...bloc k...3 Thread-0...bloc k...2 Thread-0...bloc k...1
发现线程问题被解决了!所以验证了同步函数使用的同步锁就是 this!
虽然同步函数书写较为简单(作为同步代码块的简写形式,二者功能一致),但是建议使用同步代码块,因为同步函数使用的锁唯一,而同步代码块可以使用任意对象作为锁,只在需要的若干条语句自由加锁,直观。
但是,注意到,如果需要同步的函数是静态的呢?因为静态函数属于类而不是具体对象,所以静态函数中是不存在this的,所以如果同步函数是静态的,锁显然就不是this!!!
继续按照上面思路验证,在上面程序的基础上,把同步函数改为静态,同时由于静态函数使用了num, 所以把num也改为静态,改动如下:
private static int num = 100; .... public static synchronized void show()
运行如下:
Thread-0...block...100 Thread-0...block...99 Thread-0...block...98 Thread-1...function...97 Thread-1...function...96 Thread-0...block...95 Thread-0...block...94 Thread-1...function...93 Thread-1...function...92 Thread-0...block...91 Thread-0...block...89 Thread-1...function...90 Thread-1...function...88 Thread-0...block...87 Thread-1...function...86 Thread-0...block...86 Thread-1...function...85 Thread-0...block...85 Thread-1...function...84 Thread-0...block...83 Thread-1...function...82 Thread-0...block...81 Thread-1...function...80 Thread-0...block...80 Thread-0...block...79 Thread-1...function...78 Thread-1...function...77 Thread-0...block...77 Thread-0...block...76 Thread-1...function...75 Thread-0...block...74 Thread-1...function...73 Thread-1...function...72 Thread-0...block...72 Thread-0...block...71 Thread-1...function...71 Thread-1...function...70 Thread-0...block...69 Thread-1...function...68 Thread-0...block...67 Thread-1...function...66 Thread-0...block...65 Thread-0...block...64 Thread-1...function...63 Thread-0...block...62 Thread-1...function...61 Thread-1...function...60 Thread-0...block...59 Thread-1...function...58 Thread-0...block...57 Thread-1...function...56 Thread-0...block...55 Thread-1...function...53 Thread-0...block...54 Thread-0...block...52 Thread-1...function...52 Thread-0...block...51 Thread-1...function...50 Thread-1...function...49 Thread-0...block...48 Thread-1...function...47 Thread-0...block...46 Thread-1...function...45 Thread-0...block...44 Thread-0...block...43 Thread-1...function...42 Thread-0...block...41 Thread-1...function...40 Thread-1...function...39 Thread-0...block...38 Thread-0...block...37 Thread-1...function...36 Thread-0...block...35 Thread-1...function...34 Thread-1...function...33 Thread-0...block...32 Thread-0...block...31 Thread-1...function...30 Thread-0...block...29 Thread-1...function...28 Thread-0...block...27 Thread-1...function...26 Thread-1...function...25 Thread-0...block...24 Thread-0...block...23 Thread-1...function...22 Thread-0...block...21 Thread-1...function...20 Thread-1...function...19 Thread-0...block...18 Thread-0...block...17 Thread-1...function...16 Thread-0...block...15 Thread-1...function...14 Thread-1...function...13 Thread-0...block...12 Thread-0...block...11 Thread-1...function...10 Thread-0...block...9 Thread-1...function...8 Thread-1...function...7 Thread-0...block...6 Thread-1...function...5 Thread-0...block...4 Thread-0...block...3 Thread-1...function...2 Thread-1...function...1 Thread-0...block...0
看到86,85 ,0等处又出现了线程安全问题。这说明,显然静态函数使用的不是锁不是this, 当然静态函数不可能持有this的,与我们预料的一样!
然而每一个类都属于它所在的字节码文件对象,虽然静态函数不属于具体的对象,而是属于一个类,所以静态函数必然持有字节码文件的对象,显然这个对象是静态的!
于是在上面改动的基础上再作如下改动:
synchronized(Ticket_2.class) ... ...
运行结果:
Thread-0...block...100 Thread-0...block...99 Thread-0...block...98 Thread-0...block...97 Thread-1...function...96 Thread-1...function...95 Thread-1...function...94 Thread-1...function...93 Thread-1...function...92 Thread-1...function...91 Thread-1...function...90 Thread-1...function...89 Thread-1...function...88 Thread-1...function...87 Thread-1...function...86 Thread-1...function...85 Thread-1...function...84 Thread-1...function...83 Thread-1...function...82 Thread-1...function...81 Thread-1...function...80 Thread-1...function...79 Thread-1...function...78 Thread-1...function...77 Thread-1...function...76 Thread-1...function...75 Thread-1...function...74 Thread-1...function...73 Thread-1...function...72 Thread-1...function...71 Thread-1...function...70 Thread-1...function...69 Thread-0...block...68 Thread-0...block...67 Thread-0...block...66 Thread-0...block...65 Thread-0...block...64 Thread-1...function...63 Thread-1...function...62 Thread-1...function...61 Thread-1...function...60 Thread-1...function...59 Thread-1...function...58 Thread-1...function...57 Thread-1...function...56 Thread-1...function...55 Thread-1...function...54 Thread-1...function...53 Thread-1...function...52 Thread-1...function...51 Thread-1...function...50 Thread-1...function...49 Thread-1...function...48 Thread-1...function...47 Thread-1...function...46 Thread-1...function...45 Thread-1...function...44 Thread-1...function...43 Thread-1...function...42 Thread-1...function...41 Thread-1...function...40 Thread-1...function...39 Thread-1...function...38 Thread-1...function...37 Thread-1...function...36 Thread-1...function...35 Thread-1...function...34 Thread-1...function...33 Thread-1...function...32 Thread-1...function...31 Thread-1...function...30 Thread-1...function...29 Thread-1...function...28 Thread-1...function...27 Thread-1...function...26 Thread-1...function...25 Thread-1...function...24 Thread-1...function...23 Thread-1...function...22 Thread-1...function...21 Thread-1...function...20 Thread-1...function...19 Thread-1...function...18 Thread-1...function...17 Thread-1...function...16 Thread-1...function...15 Thread-1...function...14 Thread-1...function...13 Thread-1...function...12 Thread-1...function...11 Thread-1...function...10 Thread-1...function...9 Thread-1...function...8 Thread-1...function...7 Thread-1...function...6 Thread-1...function...5 Thread-1...function...4 Thread-1...function...3 Thread-1...function...2 Thread-1...function...1
所以静态的同步函数使用的锁是 该函数所述的字节码文件对象,该对象可以用 getClass方法获取,也可以用当前 类名.class 表示,但是此时由于getClass()是非静态方法,所以只能用类名.class。
十六、单例模式的线程问题的解决方案 目录
1 package thread.demo; 2 //单例模式 3 //饿汉式 4 /* 5 class Single 6 { 7 private static final Single s = new Single(); 8 private Single(){} 9 public static Single getInstance() 10 { 11 return s; 12 } 13 } 14 */ 15 16 //懒汉式 17 18 class Single_l 19 { 20 private static Single_l s = null; 21 private Single_l(){} 22 public static Single_l getInstance() 23 { 24 if (s == null) 25 { 26 synchronized(Single_l.class) 27 { 28 if (s == null) 29 s = new Single_l(); 30 } 31 } 32 return s; 33 } 34 } 35 36 37 public class SingleDemo { 38 39 /** 40 * @param args 41 */ 42 public static void main(String[] args) { 43 // TODO Auto-generated method stub 44 45 } 46 47 }
由于懒汉式单例模式的同步代码块的代码有多条,所以可能会出现线程安全问题,所以需要进行同步。
十七、多线程死锁示例 目录
1 package thread.demo; 2 //买票:四个售票员一起卖100张票 3 class Ticket_3 implements Runnable 4 { 5 private int num = 100; 6 Object obj = new Object(); 7 boolean flag = true; 8 public void run() 9 { 10 if (flag) 11 { 12 while (true) 13 { 14 synchronized(obj) 15 { 16 show();//同步代码块中使用同步函数 17 } 18 } 19 }//end if 20 else 21 while (true) show(); 22 } 23 24 public synchronized void show() 25 { 26 synchronized(obj)//同步函数中使用同步代码块 27 { 28 if (num > 0) 29 { 30 // 让线程sleep一会,好让打印语句还没来得及执行,其他线程 31 // 就切换进来,这样方便我们观测线程安全隐患 32 try { 33 Thread.sleep(10); 34 } catch (InterruptedException e) { 35 // TODO Auto-generated catch block 36 e.printStackTrace(); 37 } 38 System.out.println(Thread.currentThread().getName() + "...function..." + num--); 39 } //end if 40 }//end synchronized 41 } 42 } 43 44 public class DeadLock 45 { 46 public static void main(String[] args) 47 { 48 49 Ticket_3 t = new Ticket_3(); 50 51 Thread seller1 = new Thread(t); 52 Thread seller2 = new Thread(t); 53 54 55 seller1.start(); //在同步代码块执行 56 57 // 主线程停顿一会,给线程0执行的机会!! 58 try { 59 Thread.sleep(20); 60 } catch (InterruptedException e) { 61 // TODO Auto-generated catch block 62 e.printStackTrace(); 63 } 64 65 t.flag = false; // 标志变为false,使得下一个线程在同步函数执行 66 seller2.start(); 67 68 } 69 }
程序解释:这是一个同步锁嵌套的情况,可以看到在第14行中的同步代码块中使用了同步函数,而在第24行的同步函数中使用了同步代码块,即两个同步锁互相嵌套。下面分析程序的运行:
- 程序从主函数开始,运行到58行之后,主线程停顿,线程0得到cpu执行权,flag = true, 执行到14 行,线程 0 拿到同步锁 obj, 执行同步代码块,而同步代码块的语句就是调用24行的同步函数,而同步函数里面是26行开始的同步代码块,执行同步代码块,就需要判断锁 obj, 线程0确实持有锁 obj,于是顺利进入26行开始的同步代码块执行,
- 线程0执行一会,由于代码块中有sleep的存在,主线程有可能切入进来占有cpu执行权,线程0就停顿一会,主线程继续由65行开始执行,flag = false, 线程1开启,然后主线程,0线程,1线程随机切换。
- 假如刚才停顿的线程0又抢到了cpu执行权,线程0继续执行,再次拿到同步代码块锁obj, 进入到第15行,存在一种情况,就在此时线程1抢到cpu执行权,线程0则持有着代码块的锁obj,并瞅准机会去拿到同步函数的锁this从16行出进入同步函数.
- 线程1获得cpu执行权之后,由于flag == false, 执行到第21行,拿到同步函数锁this进入同步函数show(), 然后在第26行准备拿锁obj的时候,却发现被obj被线程0占有,只能等待,而线程0即使抢到了cpu执行权,但是同步函数锁this却被线程1占有,也没法继续,于是程序就停顿不前,这就是死锁产生的一个过程!
多次执行上面的代码,会出现类似下面的情况:
即程序陷入了死锁,无法继续运行,所有线程0和线程1都处于等待状态!!
上面的程序只展示了死锁的样子,下面给出一个较为简单的死锁示例备用:
1 package thread.demo; 2 class MyLock 3 { 4 //“两把锁” 5 public static final Object locka = new Object(); 6 public static final Object lockb = new Object(); 7 } 8 class Test implements Runnable 9 { 10 private boolean flag; 11 Test(boolean flag) 12 { 13 this.flag = flag; 14 } 15 @Override 16 public void run() 17 { 18 if (flag) 19 { 20 synchronized(MyLock.locka) 21 { 22 System.out.println("if...loacka"); 23 synchronized(MyLock.lockb) 24 { 25 System.out.println("if...loackb"); 26 } 27 } 28 } 29 else 30 { 31 synchronized(MyLock.lockb) 32 { 33 System.out.println("else...loackb"); 34 synchronized(MyLock.locka) 35 { 36 System.out.println("else...loacka"); 37 } 38 } 39 } 40 } 41 42 } 43 public class DeadLockTest { 44 45 /** 46 * @param args 47 */ 48 public static void main(String[] args) { 49 Test a = new Test(true); 50 Test b = new Test(false); 51 Thread t1 = new Thread(a); 52 Thread t2 = new Thread(b); 53 54 t1.start(); 55 t2.start(); 56 } 57 58 }
运行结果:
后续:下一篇博文将会记载多线程间通信的学习笔记。
参考文献:传智播客JAVA SE视频教程,李刚《疯狂JAVA讲义》
</div>