首页 > 学院 > 开发设计 > 正文

Java多线程技术学习笔记(一)

2019-11-14 23:05:47
字体:
来源:转载
供稿:网友
java多线程技术学习笔记(一)

目录:

  1. 概述
  2. 多线程的好处与弊端
  3. JVM中的多线程解析
  4. 多线程的创建方式之一:继承Thread类
  5. 线程的状态
  6. 多线程创建的方式之二:实现Runnable接口
  7. 使用方式二创建多线程的好处
  8. 多线程示例
  9. 线程安全问题现象
  10. 线程安全问题产生的原因
  11. 同步代码块
  12. 同步的好处与弊端
  13. 同步的前提
  14. 同步函数
  15. 验证同步函数的锁
  16. 单例模式的线程安全问题的解决方案
  17. 死锁示例

一、概述 目录

首先得了解进程,打开我们电脑的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 args12      */13     public static void main(String[] args) {14         // TODO Auto-generated method stub15         new Demo();16         new Demo();17         System.gc();18         new Demo();19         System.out.println("Hello World!"); 20     }21 22 }

运行结果:

Hello World!demo okdemo ok

可以发现,虽然第19行在第17行代码(垃圾回收)之前,但是第19行代码却先执行,怎么回事呢?因为垃圾回收(第17行)是由垃圾回收线程执行,而第19行代码主线程的部分,cpu从主线程开始执行,然后在主线程和垃圾回收线程之间切换,创建完两个Demo()对象之后,虽然我们调用垃圾回收器,但是垃圾回收程序还没来得及执行,cpu切换到了主线程,于是先打印出了“Hello World!”

于是虽然第19行代码,执行完了,看似整个程序都执行完了,但是JVM(Java 虚拟机)还没有结束,即虽然主线程结束了,但是JVM还要执行垃圾回收线程。

四、多线程创建方式之一:继承Thread类目录

首先看一看一个简单的打印程序:

 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 args18      */19     public static void main(String[] args) {20         // TODO Auto-generated method stub21         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

运行结果:

旺财...i0旺财...i1旺财...i2旺财...i3旺财...i4旺财...i5旺财...i6旺财...i7旺财...i8旺财...i9xiaoqiang...i0xiaoqiang...i1xiaoqiang...i2xiaoqiang...i3xiaoqiang...i4xiaoqiang...i5xiaoqiang...i6xiaoqiang...i7xiaoqiang...i8xiaoqiang...i9
View Code

这时显示的只是主线程运行的结果,很容易理解!下面通过一种方式,让“旺财”和“xiaoqiang”的打印能够分别运行在不同的线程中,首先查看java的API文档:

翻译:线程 是程序中的执行线程。Java 虚拟机允许应用程序并发地执行多个执行线程。

接着文档中给出了创建线程的方法:

总结出来就是继承Thread类然后重写Thread类中的run()方法,run方法中的代码就是线程要执行的代码,然后调用start方法开启一个线程。

 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 args21      */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

运行结果:

xiaoqiang...i0旺财...i0xiaoqiang...i1旺财...i1旺财...i2旺财...i3xiaoqiang...i2xiaoqiang...i3xiaoqiang...i4xiaoqiang...i5xiaoqiang...i6xiaoqiang...i7旺财...i4xiaoqiang...i8xiaoqiang...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 args21      */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 }

运行结果:

overxiaoqiang...i0旺财...i0xiaoqiang...i1旺财...i1xiaoqiang...i2旺财...i2xiaoqiang...i3xiaoqiang...i4旺财...i3xiaoqiang...i5旺财...i4xiaoqiang...i6旺财...i5xiaoqiang...i7旺财...i6xiaoqiang...i8旺财...i7xiaoqiang...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 args21      */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-0xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...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 args19          */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-1Thread-0Thread-1Thread-0Thread-1Thread-0Thread-0Thread-1Thread-0Thread-1Thread-0Thread-1Thread-0Thread-1Thread-0Thread-1Thread-1Thread-1Thread-0Thread-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...100Thread-3...sale...97Thread-2...sale...98Thread-1...sale...99Thread-2...sale...94Thread-3...sale...95Thread-0...sale...96Thread-3...sale...91Thread-2...sale...92Thread-2...sale...88Thread-2...sale...87Thread-2...sale...86Thread-1...sale...93Thread-1...sale...84Thread-1...sale...83Thread-2...sale...85Thread-3...sale...89Thread-0...sale...90Thread-3...sale...80Thread-2...sale...81Thread-1...sale...82Thread-1...sale...76Thread-1...sale...75Thread-2...sale...77Thread-3...sale...78Thread-0...sale...79Thread-3...sale...72Thread-2...sale...73Thread-1...sale...74Thread-2...sale...69Thread-3...sale...70Thread-0...sale...71Thread-3...sale...66Thread-2...sale...67Thread-1...sale...68Thread-2...sale...63Thread-2...sale...61Thread-3...sale...64Thread-0...sale...65Thread-3...sale...59Thread-2...sale...60Thread-1...sale...62Thread-2...sale...56Thread-3...sale...57Thread-0...sale...58Thread-3...sale...53Thread-2...sale...54Thread-1...sale...55Thread-1...sale...49Thread-2...sale...50Thread-3...sale...51Thread-0...sale...52Thread-0...sale...45Thread-3...sale...46Thread-2...sale...47Thread-1...sale...48Thread-2...sale...42Thread-3...sale...43Thread-0...sale...44Thread-3...sale...39Thread-2...sale...40Thread-1...sale...41Thread-2...sale...36Thread-3...sale...37Thread-0...sale...38Thread-3...sale...33Thread-2...sale...34Thread-1...sale...35Thread-2...sale...30Thread-3...sale...31Thread-0...sale...32Thread-3...sale...27Thread-2...sale...28Thread-1...sale...29Thread-2...sale...24Thread-3...sale...25Thread-0...sale...26Thread-3...sale...21Thread-2...sale...22Thread-1...sale...23Thread-2...sale...18Thread-3...sale...19Thread-0...sale...20Thread-3...sale...15Thread-2...sale...16Thread-1...sale...17Thread-2...sale...12Thread-3...sale...13Thread-0...sale...14Thread-3...sale...9Thread-2...sale...10Thread-1...sale...11Thread-2...sale...6Thread-3...sale...7Thread-0...sale...8Thread-3...sale...3Thread-2...sale...4Thread-1...sale...5Thread-3...sale...1Thread-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 block14                     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...100Thread-2...sale...98Thread-1...sale...100Thread-0...sale...99Thread-0...sale...95Thread-3...sale...97Thread-2...sale...97Thread-1...sale...96Thread-2...sale...94Thread-3...sale...94Thread-1...sale...94Thread-0...sale...94Thread-0...sale...93Thread-3...sale...91Thread-1...sale...92Thread-2...sale...93Thread-3...sale...88Thread-0...sale...89Thread-2...sale...89Thread-1...sale...90Thread-0...sale...87Thread-2...sale...84Thread-3...sale...85Thread-1...sale...86Thread-2...sale...83Thread-3...sale...82Thread-1...sale...82Thread-0...sale...83Thread-0...sale...81Thread-3...sale...80Thread-1...sale...80Thread-2...sale...81Thread-3...sale...78Thread-0...sale...79Thread-1...sale...79Thread-2...sale...77Thread-2...sale...76Thread-1...sale...75Thread-3...sale...75Thread-0...sale...76Thread-2...sale...74Thread-1...sale...73Thread-3...sale...73Thread-0...sale...74Thread-0...sale...72Thread-1...sale...70Thread-3...sale...71Thread-2...sale...69Thread-0...sale...68Thread-2...sale...65Thread-1...sale...67Thread-3...sale...66Thread-1...sale...64Thread-2...sale...61Thread-0...sale...62Thread-3...sale...63Thread-2...sale...59Thread-1...sale...57Thread-3...sale...60Thread-0...sale...58Thread-2...sale...56Thread-1...sale...55Thread-3...sale...54Thread-0...sale...53Thread-1...sale...52Thread-2...sale...51Thread-0...sale...50Thread-3...sale...49Thread-2...sale...48Thread-1...sale...47Thread-3...sale...46Thread-0...sale...45Thread-1...sale...44Thread-3...sale...42Thread-0...sale...43Thread-2...sale...41Thread-1...sale...39Thread-0...sale...38Thread-3...sale...40Thread-2...sale...37Thread-2...sale...36Thread-0...sale...34Thread-3...sale...35Thread-1...sale...33Thread-1...sale...32Thread-2...sale...31Thread-3...sale...30Thread-0...sale...29Thread-0...sale...28Thread-3...sale...27Thread-1...sale...26Thread-2...sale...25Thread-1...sale...24Thread-2...sale...23Thread-0...sale...22Thread-3...sale...21Thread-3...sale...20Thread-1...sale...18Thread-2...sale...19Thread-0...sale...20Thread-2...sale...17Thread-3...sale...15Thread-0...sale...16Thread-1...sale...14Thread-3...sale...13Thread-0...sale...12Thread-2...sale...11Thread-1...sale...10Thread-0...sale...9Thread-3...sale...8Thread-1...sale...7Thread-2...sale...6Thread-3...sale...5Thread-0...sale...4Thread-2...sale...3Thread-1...sale...2Thread-3...sale...1Thread-0...sale...0Thread-2...sale...-1Thread-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 block16                         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...100Thread-0...sale...99Thread-0...sale...98Thread-0...sale...97Thread-0...sale...96Thread-0...sale...95Thread-0...sale...94Thread-0...sale...93Thread-0...sale...92Thread-0...sale...91Thread-0...sale...90Thread-0...sale...89Thread-0...sale...88Thread-2...sale...87Thread-2...sale...86Thread-2...sale...85Thread-3...sale...84Thread-3...sale...83Thread-3...sale...82Thread-3...sale...81Thread-1...sale...80Thread-1...sale...79Thread-1...sale...78Thread-1...sale...77Thread-1...sale...76Thread-1...sale...75Thread-3...sale...74Thread-2...sale...73Thread-2...sale...72Thread-2...sale...71Thread-2...sale...70Thread-2...sale...69Thread-2...sale...68Thread-0...sale...67Thread-0...sale...66Thread-0...sale...65Thread-0...sale...64Thread-0...sale...63Thread-0...sale...62Thread-2...sale...61Thread-2...sale...60Thread-3...sale...59Thread-3...sale...58Thread-3...sale...57Thread-3...sale...56Thread-3...sale...55Thread-3...sale...54Thread-3...sale...53Thread-3...sale...52Thread-3...sale...51Thread-3...sale...50Thread-3...sale...49Thread-3...sale...48Thread-3...sale...47Thread-3...sale...46Thread-3...sale...45Thread-3...sale...44Thread-3...sale...43Thread-3...sale...42Thread-3...sale...41Thread-1...sale...40Thread-1...sale...39Thread-1...sale...38Thread-1...sale...37Thread-1...sale...36Thread-1...sale...35Thread-1...sale...34Thread-1...sale...33Thread-1...sale...32Thread-1...sale...31Thread-1...sale...30Thread-1...sale...29Thread-1...sale...28Thread-1...sale...27Thread-1...sale...26Thread-1...sale...25Thread-3...sale...24Thread-2...sale...23Thread-2...sale...22Thread-2...sale...21Thread-0...sale...20Thread-0...sale...19Thread-0...sale...18Thread-0...sale...17Thread-2...sale...16Thread-2...sale...15Thread-2...sale...14Thread-2...sale...13Thread-3...sale...12Thread-3...sale...11Thread-3...sale...10Thread-3...sale...9Thread-3...sale...8Thread-3...sale...7Thread-1...sale...6Thread-1...sale...5Thread-1...sale...4Thread-1...sale...3Thread-3...sale...2Thread-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 args26      */27     public static void main(String[] args) {28         // TODO Auto-generated method stub29         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 = 100sum = 200sum = 300sum = 500sum = 400sum = 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 = 200sum = 200sum = 400sum = 400sum = 600sum = 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 block11                 e.printStackTrace();12             }13             System.out.println("sum = "  + sum);14         }15     }16 }

运行结果:

sum = 100sum = 200sum = 300sum = 400sum = 500sum = 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 block11                 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 block22                             e.printStackTrace();23                         }24                         System.out.println(Thread.currentThread().getName() + "...bloc k..." + num--);25                     }26                 }27             }28                 29         }//end if30         else31             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 block44                 e.printStackTrace();45             }46             System.out.println(Thread.currentThread().getName() + "...function..." + num--);47         }48     } 49 }50 51 public class SynFunctionLockDemo52 {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...100Thread-0...function...99Thread-0...function...98Thread-0...function...97Thread-0...function...96Thread-0...function...95Thread-0...function...94Thread-0...function...93Thread-0...function...92Thread-0...function...91Thread-0...function...90Thread-0...function...89Thread-0...function...88Thread-0...function...87Thread-0...function...86Thread-0...function...85Thread-1...function...84Thread-1...function...83Thread-1...function...82Thread-1...function...81Thread-0...function...80Thread-0...function...79Thread-0...function...78Thread-0...function...77Thread-0...function...76Thread-0...function...75Thread-0...function...74Thread-1...function...73Thread-1...function...72Thread-0...function...71Thread-0...function...70Thread-0...function...69Thread-1...function...68Thread-1...function...67Thread-1...function...66Thread-0...function...65Thread-0...function...64Thread-0...function...63Thread-0...function...62Thread-1...function...61Thread-1...function...60Thread-1...function...59Thread-1...function...58Thread-0...function...57Thread-0...function...56Thread-0...function...55Thread-1...function...54Thread-1...function...53Thread-1...function...52Thread-1...function...51Thread-1...function...50Thread-1...function...49Thread-1...function...48Thread-1...function...47Thread-0...function...46Thread-0...function...45Thread-0...function...44Thread-0...function...43Thread-1...function...42Thread-1...function...41Thread-1...function...40Thread-1...function...39Thread-1...function...38Thread-1...function...37Thread-1...function...36Thread-1...function...35Thread-1...function...34Thread-1...function...33Thread-1...function...32Thread-1...function...31Thread-1...function...30Thread-1...function...29Thread-1...function...28Thread-1...function...27Thread-0...function...26Thread-0...function...25Thread-0...function...24Thread-1...function...23Thread-1...function...22Thread-1...function...21Thread-1...function...20Thread-1...function...19Thread-1...functi
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表