Java多线程之线程同步

Posted zengblogs

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程之线程同步相关的知识,希望对你有一定的参考价值。

一、多线程出现的安全问题:

1、问题的原因:

多个线程执行的不确定性引起执行结果的不稳定。当多条语句在操作同一个线程共享数据时,
一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
2、解决的办法:

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

二、Synchronized的使用方法:

技术图片 

1、操作共享数据的代码,即为需要被同步的代码。不能包含代码多或者少。
2、共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
3、同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
4、要求:多个线程必须要共用同一把锁。

三、同步机制中的锁:

1、同步锁机制:

对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。
防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。
第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,
就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。

2、synchronized的锁是什么:

任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
同步方法的锁:静态方法(类名.class) 、非静态方法(this)
同步代码块:自己指定,很多时候也是指定为this或类名.class

3、注意事项:

必须确保使用同一个资源的多个线程共用一把锁,
这个非常重要,否则就无法保证共享资源的安全。
一个线程类中的所有静态方法共用同一把锁(类名.class) ,
所有非静态方法共用同一把锁(this) ,同步代码块( 指定需谨慎)

例子:创建三个窗口卖票,总票数为100张。使用实现Runnable接口的方式
1、问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题。
2、问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
3、如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,
其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
4、在Java中,我们通过同步机制,来解决线程的安全问题。

 1 class Window implements Runnable{
 2 
 3     private int ticket = 100;
 4     @Override
 5     public void run() {
 6 
 7         while(true){
 8             synchronized (this){
 9                 //此时的this:唯一的Window1的对象   
10                 if (ticket > 0) {
11 
12                     try {
13                         Thread.sleep(100);
14                     } catch (InterruptedException e) {
15                         e.printStackTrace();
16                     }
17 
18                     System.out.println(Thread.currentThread().getName() 
19                             + ":卖票,票号为:" + ticket);
20 
21                     ticket--;
22                 } else {
23                     break;
24                 }
25             }
26         }
27     }
28 }
29 
30 
31 public class WindowTest {
32     public static void main(String[] args) {
33         Window1 w = new Window1();
34 
35         Thread t1 = new Thread(w);
36         Thread t2 = new Thread(w);
37         Thread t3 = new Thread(w);
38 
39         t1.setName("窗口1");
40         t2.setName("窗口2");
41         t3.setName("窗口3");
42 
43         t1.start();
44         t2.start();
45         t3.start();
46     }
47 }

使用同步方法解决实现Runnable接口的线程安全问题。
关于同步方法的总结:
1、同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
2、非静态的同步方法,同步监视器是:this。
3、静态的同步方法,同步监视器是:当前类本身。

 1 class Window implements Runnable {
 2     private int ticket = 100;
 3     @Override
 4     public void run() {
 5         while (true) {
 6             show();
 7         }
 8     }
 9     
10     private synchronized void show(){
11         //同步监视器:this
12         //synchronized (this){
13 
14             if (ticket > 0) {
15 
16                 try {
17                     Thread.sleep(100);
18                 } catch (InterruptedException e) {
19                     e.printStackTrace();
20                 }
21 
22                 System.out.println(Thread.currentThread().getName() 
23                         + ":卖票,票号为:" + ticket);
24 
25                 ticket--;
26             }        
27     }
28 }
29 
30 
31 public class WindowTest {
32     public static void main(String[] args) {
33         Window3 w = new Window3();
34 
35         Thread t1 = new Thread(w);
36         Thread t2 = new Thread(w);
37         Thread t3 = new Thread(w);
38 
39         t1.setName("窗口1");
40         t2.setName("窗口2");
41         t3.setName("窗口3");
42 
43         t1.start();
44         t2.start();
45         t3.start();
46     }
47 }

四、线程同步的范围:

1、如何找问题,即代码是否存在线程安全 (非常重要 )
(1)明确哪些代码是多线程运行的代码。
(2)明确多个线程是否有共享数据。
(3)明确多线程运行代码中是否有多条语句操作共享数据。

2、如何解决呢 (非常重要)
对多条操作共享数据的语句,只能让一个线程都执行完,
在执行过程中,其他线程不可以参与执行。
即所有操作共享数据的这些语句都要放在同步范围中。

3、切记:
范围太小:没锁住所有有安全问题的代码
范围太大:没发挥多线程的功能。

4、释放锁的操作:
●当前线程的同步方法、同步代码块执行结束。
●当前线程在同步代码块、同步方法中遇到break、returm终 止了该代码块、该方法的继续执行。
●当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
●当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。 

5、不会释放锁的操作:
●线程执行同步代码块或同步方法时,程序调用Thread. sleep()、Thread.yield()方法暂停当前线程的执行。
●线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,
该线程不会释放锁(同步监视器),应尽量避免使用suspend()和resume()来控制线程。

6、线程的死锁问题:
●死锁原因:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,
就形成线程死锁。出现死锁后,不会出现异常或提示,只是所有的线程都处于阻塞状态,无法继续。
●解决方法:
专门的算法或原则、尽量减少同步资源的定义、尽量避免嵌套同步。

五、Lock(锁):

1、从JDK 5.0开始,Java提供 了更强大的线程同步机制一通过显式定 义同
步锁对象来实现同步。同步锁使用Lock对象充当。

2、java.util.concurrent locks.Lock接口是控制多个线程对共享资源进行访问的
工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象
加锁,线程开始访问共享资源之前应先获得Lock对象。

3、Reentrantl _ock类实现了Lock ,它拥有与synchronized相同的并发性和
内存语义,在实现线程安全的控制中,比较常用的是Reentrantl _ock,可以
显式加锁、释放锁。

技术图片 

4、synchronized与Lock的对比:
(1)Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁, 出了作用域自动释放。
(2)Lock只有代码块锁,synchronized有 代码块锁和方法锁。
(3)使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
(4)优先使用顺序:Lock→同步代码块(已经进入了方法体,分配了相应资源)→同步方法(在方法体之外)。

以上是关于Java多线程之线程同步的主要内容,如果未能解决你的问题,请参考以下文章

Java基础之多线程

java基础入门-多线程同步浅析-以银行转账为样例

Java多线程之线程同步

多线程 Thread 线程同步 synchronized

Java多线程之synchronized及其优化

Java并发编程之线程安全线程通信