zoukankan      html  css  js  c++  java
  • 2.3多线程(java学习笔记)synchronized关键字

    一、为什么要用synchronized关键字

    首先多线程中多个线程运行面临共享数据同步的问题。

    多线程正常使用共享数据时需要经过以下步骤:

    1.线程A从共享数据区中复制出数据副本,然后处理。

    2.线程A将处理好的数据副本写入共享数据区。

    3.线程B从共享数据区中复制出数据副本。

    如此循环,直到线程结束。

     假如线程A从共享数据区中复制出数据副本然后处理,在还没有将更新的数据放入主内存时,线程B来到主内存读取了未更新的数据,这样就出问题了。

    这就是所谓的脏读,这类问题称为多线程的并发问题。

    举个具体的例子:

     1 public class TestThread {
     2      public static void main(String[] args){
     3          TestSynchronized s = new TestSynchronized();
     4          new Thread(s,"t1").start();    //两个线程访问一个对象
     5          new Thread(s,"t2").start();
     6      }
     7 }
     8 
     9 class TestSynchronized implements Runnable{
    10     private int ticket = 5;
    11     
    12     public void run(){    
    13             for(int p = 0; p < 10; p++){
    14                 try {
    15                     Thread.sleep(500);
    16                 } catch (InterruptedException e) {
    17                     // TODO Auto-generated catch block
    18                     e.printStackTrace();
    19                 }
    20                 if(ticket >= 0){
    21                     System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
    22                 }
    23             }
    24     }
    25 }
    运行结果:
    t2 ticket:4
    t1 ticket:5
    t1 ticket:2
    t2 ticket:3
    t1 ticket:1
    t2 ticket:1
    t2 ticket:0

    可以看到1号票同时给了t1和t2,当t1读入1执行了ticket--后,数据还没有来得及写入主内存就被t2从主内存中读走了1,就造成了这种现象。

    要想避免这种现象就需要使用synchronized关键字,synchronized英译为同步,我们认为暂且把他看做锁定更好理解。

    接下来我们看看synchronized如何使用。

    二、synchronized的用法

    1. synchronized修饰方法(也称同步方法)

    (1) java中每个对象都有一个锁(lock),或者叫做监视器,当前线程访问某个对象中synchronized修饰的方法(同步块)时,线程需要获取到该对象的锁,获取对象锁后才能访问该对象中synchronized方法(同步块),且一个对象中只有一个锁。

    (2) 没有获得该对象的锁的其他线程,无法访问该对象中synchronized修饰的方法(同步块)。

    (3) 其他线程要想访问该对象中synchronized修饰的方法需要获取该对象的锁。

    (4) 对象锁只有将synchronized方法(同步块)中的内容运行完毕或遇到异常才会释放锁。

     例一:

     1 public class TestThread {
     2      public static void main(String[] args){
     3          TestSynchronized s = new TestSynchronized();
     4          new Thread(s,"t1").start();     //两个线程访问一个对象
     5          new Thread(s,"t2").start();
     6      }
     7 }
     8 
     9 class TestSynchronized implements Runnable{
    10     private int ticket = 5;
    11     
    12     synchronized public void run(){    
    13             for(int p = 0; p < 10; p++){
    14                 try {
    15                     Thread.sleep(500);
    16                 } catch (InterruptedException e) {
    17                     // TODO Auto-generated catch block
    18                     e.printStackTrace();
    19                 }
    20                 if(ticket >= 0){
    21                     System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
    22                 }                    
    23             }
    24         }
    25 }
    运行结果:
    t1 ticket:5
    t1 ticket:4
    t1 ticket:3
    t1 ticket:2
    t1 ticket:1
    t1 ticket:0

    我们来分析上面程序,首先线程t1进去run方法获得对象s的锁,然后执行完run方法释放锁,run运行忘了也就没有t2的事了。

    因为只有将synchronized修饰的方法执行完才会释放锁,故打印五个t1.。

    还有一点,如果一个对象里面有多个synchronized方法,某一时刻只能有一个线程进入其中一个synchronized修饰的方法,则这时其他任何线程无法进入该对象中任何一个synchronized修饰的方法。

    补充片段:

     1 public class TestThread {
     2     public static void main(String[] args){
     3         Test m1 = new Test();  //两个线程共访问一个对象。
     4 
     5         TestSynchronized_1 s1 = new TestSynchronized_1(m1);
     6         TestSynchronized_2 s2 = new TestSynchronized_2(m1);
     7         new Thread(s1,"t1").start();
     8         new Thread(s2,"t2").start();
     9     }
    10 }
    11 
    12 class Test{
    13     synchronized public void test1(){
    14                for(int p = 0; p < 5; p++){
    15                    System.out.println("s1.run.TestSynchronized_test 1");
    16                }   
    17     }
    18     
    19     synchronized public void test2(){
    20         for(int p = 0; p < 5; p++){
    21             System.out.println("s2.run.TestSynchronized_test 2");
    22         }   
    23 }
    24 }
    25 
    26 class TestSynchronized_1 implements Runnable{
    27    
    28    private Test m;
    29    public TestSynchronized_1(Test m){
    30       this.m = m;
    31    }
    32    
    33    public void run(){
    34        m.test1();
    35    }
    36 }
    37 
    38 class TestSynchronized_2 implements Runnable{
    39        
    40        private Test m;
    41        public TestSynchronized_2(Test m){
    42           this.m = m;
    43        }
    44        
    45        public void run(){
    46            m.test2();
    47        }
    48 }
    运行结果:
    s1.run.TestSynchronized_test 1
    s1.run.TestSynchronized_test 1
    s1.run.TestSynchronized_test 1
    s1.run.TestSynchronized_test 1
    s1.run.TestSynchronized_test 1
    s2.run.TestSynchronized_test 2
    s2.run.TestSynchronized_test 2
    s2.run.TestSynchronized_test 2
    s2.run.TestSynchronized_test 2
    s2.run.TestSynchronized_test 2

    当线程t1运行synchronized修饰的test1方法时,线程t2是无法运行test2方法。结合之前说的,一个对象锁只有一把,而这里是两个线程共享对象(m1),当线程t1获得锁时,线程t2就只能等待。归根结底把握几个要点:

    1.锁的唯一性(一个对象只有一把锁,但不同对象就有不同的锁)

    2.没锁不能进去入synchronized修饰内容中运行。

    3.只有运行完synchronized修饰的内容或遇到异常才释放锁。

    我们来看下面这个代码:

    例二:

     1 public class TestThread {
     2      public static void main(String[] args){
     3          Mouth m1 = new Mouth();  
     4          Mouth m2 = new Mouth();  
     5          TestSynchronized s1 = new TestSynchronized(m1);//两个线程访问两个对象。
     6          TestSynchronized s2 = new TestSynchronized(m2);
     7          new Thread(s1,"t1").start();  //线程t1
     8          new Thread(s2,"t2").start();   //线程t2
     9      }
    10 }
    11 
    12 class Mouth{  //资源及方法 
    13     synchronized public void test(){
    14         int ticket = 5;
    15         for(int p = 0; p < 10; p++){
    16             try {
    17                 Thread.sleep(500);
    18             } catch (InterruptedException e) {
    19                 // TODO Auto-generated catch block
    20                 e.printStackTrace();
    21             }
    22             if(ticket >= 0){
    23                 System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
    24             }                    
    25         }
    26     }
    27 
    28 }
    29 
    30 class TestSynchronized implements Runnable{
    31     private Mouth m = new Mouth();
    32     
    33     public TestSynchronized(Mouth m){
    34         this.m = m;
    35     }
    36     
    37     synchronized public void run(){
    38         m.test();
    39     }
    40 }
    运行结果:
    t1 ticket:5
    t2 ticket:5
    t2 ticket:4
    t1 ticket:4
    t1 ticket:3
    t2 ticket:3
    t2 ticket:2
    t1 ticket:2
    t1 ticket:1
    t2 ticket:1
    t2 ticket:0
    t1 ticket:0

    可以发现好像用synchronized修饰的test方法没有起作用,怎么是t1,t2怎么是交替运行的?

    我们回顾下之前说的对象锁,线程获得对象锁后可以访问该对象里面synchronized修饰的方法,其他线程无法访问。

    我们上面的代码里面对象有两个,一个是m1、一个是m2。

    t1获得了对象m1的锁,然后访问m1中的test方法;t2获得了对象m2的锁,然后访问s2中的test方法。

    线程t1和线程t2访问的是不同的资源(m1,m2),并不相互干扰所以没有影响。例一中是因为两个线程访问同一个资源(s1)所以synchronized的起了限制作用。

    synchronized修饰方法时只能对多个线程访问同一资源(对象)时起限制作用。

    可能大家会说了,那我们有没有办法也限制下这种情况呢,答案当然是可以的。

    这就是下面要说的:

    2.synchronized修饰静态方法

     当修饰静态方法时锁定的是,而不是对象,我们先把例二修改下看下结果。

    例三:

     1 public class TestThread {
     2      public static void main(String[] args){
     3          Mouth m1 = new Mouth();
     4          Mouth m2 = new Mouth();
     5          TestSynchronized s1 = new TestSynchronized(m1);
     6          TestSynchronized s2 = new TestSynchronized(m2); //两个线程访问两个对象
     7          new Thread(s1,"t1").start();
     8          new Thread(s2,"t2").start();
     9      }
    10 }
    11 
    12 class Mouth{
    13     synchronized public static void test(){ //改为静态方法,锁定的是类。
    14         int ticket = 5;
    15         for(int p = 0; p < 10; p++){
    16             try {
    17                 Thread.sleep(500);
    18             } catch (InterruptedException e) {
    19                 // TODO Auto-generated catch block
    20                 e.printStackTrace();
    21             }
    22             if(ticket >= 0){
    23                 System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
    24             }                    
    25         }
    26     }
    27 
    28 }
    29 
    30 class TestSynchronized implements Runnable{
    31     private Mouth m = new Mouth();
    32     
    33     public TestSynchronized(Mouth m){
    34         this.m = m;
    35     }
    36     
    37     synchronized public void run(){
    38         m.test();
    39     }
    40 }
    运行结果:
    t1 ticket:5
    t1 ticket:4
    t1 ticket:3
    t1 ticket:2
    t1 ticket:1
    t1 ticket:0
    t2 ticket:5
    t2 ticket:4
    t2 ticket:3
    t2 ticket:2
    t2 ticket:1
    t2 ticket:0

    当synchronized修饰静态方法时,线程需要获得类(Mouth)锁才能运行,没有获得类锁的线程无法运行,且获得类锁的线程会将synchronized修饰的静态方法会运行完毕才释放类锁。

    例如例三中的代码,t1先获得类(Mouth)锁运行Mouth类中的test方法,而t2没有类(Mouth)锁就无法运行。一个类只有一个类锁,却可以有多个对象(t1,t2等...)都是一个类(Mouth)中的对象,只要一个线程获取了类(Mouth)锁,其他线程就要等到类锁被释放,然后获得类(Mouth)锁之后才能运行类(Mouth)中synchronized修饰的静态方法。所以即使是两个线程(t1,t2)访问两个不同的资源(m1,m2)也会受到限制,因为m1,m2都属于一个类(Mouth),而锁住类(Mouth)后每次只能有一个线程访问该类(Mouth)中的sychronized修饰的静态方法。

    当t1访问m1中的test时,首先获得类(Mouth)锁,这时如果t2访问m2中的test方法时也需要获得类锁,可是这时类锁已经被线程t1获得,故t2无法访问m2中的方法。只有等t1运行完方法中的内容或异常释放锁后t2才有机会获得锁,获得锁后才能运行。

    而之前例一中t1,t2锁的是对象,需要结合这几段代码理解下。

    3.synchronized块(也称同步块)

    如果每次都锁定的范围都是一个方法,每次只能有一个线程进去势必会导致效率的低下,这主要是锁定范围过多引起的。

    这时可以根据实际情况锁定合适的区域,这就要用到同步块了

    synchronized(需要锁住的对象或类){
           
           锁定的部分,需要锁才能运行。
    }

     ()中可以确定锁定的是对象还是类,锁定对象的话可以用this,对类上锁类名加class,例如要锁定Mounth类(Moutn.class)。

    我们首先看个没有任何同步的例子:

     1 public class TestThread {
     2      public static void main(String[] args){
     3          TestSynchronized s1 = new TestSynchronized();
     4     
     5          new Thread(s1,"t1").start();  //两个线程访问一个对象
     6          new Thread(s1,"t2").start();
     7      }
     8 }
     9 
    10 class TestSynchronized implements Runnable{
    11     private int ticket = 5;
    12     
    13            public void run(){    
    14             for(int p = 0; p < 10; p++){
    15                 try {
    16                     Thread.sleep(1000);
    17                 } catch (InterruptedException e) {
    18                     // TODO Auto-generated catch block
    19                     e.printStackTrace();
    20                 }
    21                 if(ticket >= 0){
    22                     System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
    23                 }                    
    24             }
    25         }
    26 }
    运行结果:
    t1 ticket:5
    t2 ticket:4
    t2 ticket:3
    t1 ticket:2
    t2 ticket:1
    t1 ticket:1
    t2 ticket:0
    t1 ticket:-1

    其中出现了-1,我们在其中一块区域加上同步形成同步块。

     1 public class TestThread {
     2      public static void main(String[] args){
     3          TestSynchronized s1 = new TestSynchronized(); //两个线程访问一个对象
     4     
     5          new Thread(s1,"t1").start();
     6          new Thread(s1,"t2").start();
     7      }
     8 }
     9 
    10 class TestSynchronized implements Runnable{
    11     private int ticket = 5;
    12     
    13            public void run(){    
    14             for(int p = 0; p < 10; p++){
    15                 try {
    16                     Thread.sleep(1000);
    17                 } catch (InterruptedException e) {
    18                     // TODO Auto-generated catch block
    19                     e.printStackTrace();
    20                 }
    21                 synchronized(this){  //此次加上同步块,这部分内容一次只有一个线程可以进入,其他内容不受约束。
    22                     if(ticket >= 0){ //这里锁的是对象,这里面的内容需要对象锁才能运行。
    23                         System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
    24                     }
    25                 }                    
    26             }
    27         }
    28 }
    运行结果:
    t2 ticket:5
    t1 ticket:4
    t1 ticket:3
    t2 ticket:2
    t1 ticket:1
    t2 ticket:0

    一次只能有一个线程进入同步块中,就不会出现线程读了未更新的数据或者多减一次的情况。未加synchronized修饰的其他区域不受影响,故两个线程的顺序不定。

    下面我们来看一个同步块锁定的例子,效果和例三一样,只不过例三使用静态方法锁定了类,而下面这个是使用同步块锁定了类。

     1 public class TestThread {
     2     public static void main(String[] args){
     3         TestSynchronized s1 = new TestSynchronized();
     4         TestSynchronized s2 = new TestSynchronized(); //两个线程访问两个对象
     5         new Thread(s1,"t1").start();
     6         new Thread(s2,"t2").start();
     7     }
     8 }
     9 
    10 class TestSynchronized implements Runnable{
    11    private int ticket = 5;
    12    
    13    synchronized public void run(){
    14        synchronized(TestSynchronized.class){  //将synchronized修饰的静态方法改成了同步块。
    15             
    16            for(int p = 0; p < 10; p++){
    17                try {
    18                    Thread.sleep(500);
    19                } catch (InterruptedException e) {
    20                    // TODO Auto-generated catch block
    21                    e.printStackTrace();
    22                }
    23                if(ticket >= 0){
    24                    System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
    25                }                    
    26            }
    27        }
    28    }
    29 }
    运行结果:
    t1 ticket:5
    t1 ticket:4
    t1 ticket:3
    t1 ticket:2
    t1 ticket:1
    t1 ticket:0
    t2 ticket:5
    t2 ticket:4
    t2 ticket:3
    t2 ticket:2
    t2 ticket:1
    t2 ticket:0

    上述代码和例三功能一样,只是锁定方法不同,这里只是做下演示。

    synchronized修饰方法是一种粗颗粒的并发控制,某一时刻只有一个线程执行方法内的内容效率较低下。

    synchronized同步块是一种细颗粒的并发控制,可以自行根据需求确定区域较为灵活,可以平衡下效率和安全,同时也能因选择区域不恰当而造成问题。

    只要不在synchronized方法(同步块)内的其他部分都不受限制。

    普通方法锁定的对象,需要获得对象锁

    静态方法锁定的是类,需要获得类锁。

    同步块可以确定是锁对象(this )还是锁类(xxx.class),同时也可以自行确定区域。 

  • 相关阅读:
    bzoj 3779 重组病毒——LCT维护子树信息
    bzoj 4010 [HNOI2015]菜肴制作——贪心
    bzoj 2535 && bzoj 2109 [Noi2010]Plane 航空管制——贪心
    bzoj 3671 [Noi2014]随机数生成器——贪心(时间复杂度分配)
    bzoj 2395 [Balkan 2011]Timeismoney——最小乘积生成树
    bzoj 3157 && bzoj 3516 国王奇遇记——推式子
    bzoj 1101 [POI2007]Zap——反演
    hdu 4372 Count the Buildings——第一类斯特林数
    bzoj 2406 矩阵——有源汇上下界可行流
    bzoj 2039 [2009国家集训队]employ人员雇佣——二元关系
  • 原文地址:https://www.cnblogs.com/huang-changfan/p/9460073.html
Copyright © 2011-2022 走看看