zoukankan      html  css  js  c++  java
  • Java多线程:多线程的Synchronized详解

    输出为什么要引入同步机制

    在多线程环境中,可能会有两个甚至更多的线程试图同时访问一个有限的资源。必须对这种潜在资源冲突进行预防。
    解决方法:在线程使用一个资源时为其加锁即可。访问资源的第一个线程为其加上锁以后,其他线程便不能再使用那个资源,除非被解锁。

    怎样实现同步

    对于访问某个关键共享资源的所有方法,都必须把它们设为synchronized
    例如:
    synchronized void f() { /* ... */ } synchronized void g() { /* ... */ }
    如果想保护某些资源不被多个线程同时访问,可以强制通过synchronized方法访问那些资源。 调用synchronized方法时,对象就会被锁定。

     1 public class MyStack {
     2     int idx = 0;
     3     char [] data = new char[ 6];
     4     public synchronized void push( char c) {
     5       data[ idx] = c;
     6       idx++;
     7     }
     8     public synchronized char pop() {
     9       idx--;
    10       return data[ idx];
    11     }
    12 }

    说明:
    •当synchronized方法执行完或发生异常时,会自动释放锁。
    •被synchronized保护的数据应该是私有(private)的。

    看几个例子

     1 public class ThreadTest3
     2 {
     3     public static void main(String[] args)
     4     {
     5         Runnable r = new HelloThread();
     6         
     7         Thread t1 = new Thread(r);
     8         
     9         //r = new HelloThread();
    10         
    11         Thread t2 = new Thread(r);
    12         
    13         t1.start();
    14         t2.start();
    15     }
    16 }
    17 
    18 class HelloThread implements Runnable
    19 {
    20     int i;
    21     
    22     @Override
    23     public void run()
    24     {
    25     //    int i = 0;
    26         
    27         while(true)
    28         {
    29             System.out.println("number: " + i++);
    30             
    31             try
    32             {
    33                 Thread.sleep((long)(Math.random() * 1000));
    34             }
    35             catch (InterruptedException e)
    36             {
    37                 e.printStackTrace();
    38             }
    39             
    40             if(5 == i)
    41             {
    42                 break;
    43             }
    44         }
    45     }
    46 }

    输出顺序执行:

    程序执行时出现了两种输出,现在分别来分析一下:

    为什么打印两个0:如文章开头所讲,由于两个线程同时访问一个资源,而且此资源未加锁就会导致访问冲突。

    线程1打印语句还未执行++操作,线程2就进来了,所以导致打印两个0

    为什么"只有一个线程输出":

    只打印了0-4,给人感觉只打印了一条线程,其实这是两条线程共同作用的结果。

    因为 i 这个变量是一个成员变量,而在同一个对象 r 中成员变量是共享的,当线程对 i 进行++操作,线程2中的 i 也会更改,所以打印出这样的结果

    如果将 i 放在方法内即变成局部变量,则会打印两条线程的执行结果

    总结:

    关于成员变量与局部变量:如果一个变量是成员变量,那么多个线程对同一个对象的成员变量进行操作时,他们对该成员变量是彼此影响的(也就是说一个线程对成员变量的改变会影响到另一个线程)。
    如果一个变量是局部变量,那么每个线程都会有一个该局部变量的拷贝,一个线程对该局部变量的改变不会影响到其他的线程。
    停止线程的方式:不能使用Thread类的stop方法来终止线程的执行。一般要设定一个变量,在run方法中是一个循环,循环每次检查该变量,如果满足条件则继续执行,否则跳出循环,线程结束。

    synchronized关键字

    synchronized关键字:当synchronized关键字修饰一个方法的时候,该方法叫做同步方法。
    Java中的每个对象都有一个锁(lock)或者叫做监视器(monitor),当访问某个对象的synchronized方法时,表示将该对象上锁,此时其他任何线程都无法再去访问该synchronized方法了,直到之前的那个线程执行方法完毕后(或者是抛出了异常),那么将该对象的锁释放掉,其他线程才有可能再去访问该synchronized方法。

     1 public class ThreadTest4
     2 {
     3     public static void main(String[] args)
     4     {
     5         Example example = new Example();
     6         
     7         Thread t1 = new TheThread(example);
     8         
     9         example = new Example();
    10         
    11         Thread t2 = new TheThread2(example);
    12         
    13         t1.start();
    14         t2.start();
    15     }
    16 }
    17 
    18 class Example
    19 {
    20     public synchronized void execute()
    21     {
    22         for(int i = 0; i < 5; i++)
    23         {
    24             try
    25             {
    26                 Thread.sleep((long)(Math.random() * 1000));
    27             }
    28             catch (InterruptedException e)
    29             {
    30                 e.printStackTrace();
    31             }
    32             
    33             System.out.println("hello: " + i);
    34         }
    35     }
    36     
    37     public synchronized void execute2()
    38     {
    39         for(int i = 0; i < 5; i++)
    40         {
    41             try
    42             {
    43                 Thread.sleep((long)(Math.random() * 1000));
    44             }
    45             catch (InterruptedException e)
    46             {
    47                 e.printStackTrace();
    48             }
    49             
    50             System.out.println("world: " + i);
    51         }
    52     }
    53 }
    54 
    55 class TheThread extends Thread
    56 {
    57     private Example example;
    58     
    59     public TheThread(Example example)
    60     {    
    61         this.example = example;
    62     }
    63     
    64     @Override
    65     public void run()
    66     {
    67         this.example.execute();
    68     }
    69 }
    70 
    71 class TheThread2 extends Thread
    72 {
    73     private Example example;
    74     
    75     public TheThread2(Example example)
    76     {    
    77         this.example = example;
    78     }
    79     
    80     @Override
    81     public void run()
    82     {
    83         this.example.execute2();
    84     }
    85 }

    结果为

    结果可以看出两条线程都同时执行了且互不影响。

    当我们修改main方法

     1 public class ThreadTest4
     2 {
     3     public static void main(String[] args)
     4     {
     5         Example example = new Example();
     6         
     7         Thread t1 = new TheThread(example);
     8         
     9         Thread t2 = new TheThread2(example);
    10         
    11         t1.start();
    12         t2.start();
    13     }
    14 }

    使用同一个 example,结果为

    这个结果是必然的,不管你执行多少次。

    很显然,线程在执行时是一个线程一个线程的执行的。

    原因就如上面红字所讲的,当访问某个对象的synchronized方法时表示对该对象上锁,我们这里使用的是同一个example对象,所以只能等上一个线程执行完后才能执行下一个线程,而不能同时执行。

    如果一个对象有多个synchronized方法,某一时刻某个线程已经进入到了某个synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的。

    再来更改Example对象,在其方法上都加上 static 修饰

     1 public synchronized static void execute()
     2     {
     3         for(int i = 0; i < 5; i++)
     4         {
     5             try
     6             {
     7                 Thread.sleep((long)(Math.random() * 1000));
     8             }
     9             catch (InterruptedException e)
    10             {
    11                 e.printStackTrace();
    12             }
    13             
    14             System.out.println("hello: " + i);
    15         }
    16     }
    17     
    18     public synchronized static void execute2()
    19     {
    20         for(int i = 0; i < 5; i++)
    21         {
    22             try
    23             {
    24                 Thread.sleep((long)(Math.random() * 1000));
    25             }
    26             catch (InterruptedException e)
    27             {
    28                 e.printStackTrace();
    29             }
    30             
    31             System.out.println("world: " + i);
    32         }
    33     }

    这时,不管你是否使用同一个example,执行结果都是先执行线程1在执行线程2.

    原因就在于:

    如果某个synchronized方法是static的,那么当线程访问该方法时,它锁的并不是synchronized方法所在的对象,而是synchronized方法所在的对象所对应的Class对象,因为Java中无论一个类有多少个对象,这些对象会对应唯一一个Class对象,因此当线程分别访问同一个类的两个对象的两个static,synchronized方法时,他们的执行顺序也是顺序的,也就是说一个线程先去执行方法,执行完毕后另一个线程才开始执行。

    再对Example方法进行修改,改成一个static一个非static

     1     public synchronized void execute()
     2     {
     3         for(int i = 0; i < 5; i++)
     4         {
     5             try
     6             {
     7                 Thread.sleep((long)(Math.random() * 1000));
     8             }
     9             catch (InterruptedException e)
    10             {
    11                 e.printStackTrace();
    12             }
    13             
    14             System.out.println("hello: " + i);
    15         }
    16     }
    17     
    18     public synchronized static void execute2()
    19     {
    20         for(int i = 0; i < 5; i++)
    21         {
    22             try
    23             {
    24                 Thread.sleep((long)(Math.random() * 1000));
    25             }
    26             catch (InterruptedException e)
    27             {
    28                 e.printStackTrace();
    29             }
    30             
    31             System.out.println("world: " + i);
    32         }
    33     }

    这时不管你是否使用同一个example,结果都是乱序,即两个线程同时执行,原因还是在于锁对象问题。

    static锁的是class对象,非static锁的就是对象,两个锁的对象不同,结果自然不同。

    synchronized代码块

    synchronized方法是一种粗粒度的并发控制,某一时刻,只能有一个线程执行该synchronized方法;

    synchronized块则是一种细粒度的并发控制,只会将块中的代码同步,位于方法内、synchronized块之外的代码是可以被多个线程同时访问到的。

    所以更多情况下,我们选择把需要同步的逻辑写在synchronized块中。

    1 synchronized(object)
    2 {
    3       。。。          
    4 }

    表示线程在执行的时候会对object对象上锁

    所以上面的程序就可以改为

     1 public void execute()
     2     {
     3         synchronized (this)
     4         {
     5             for (int i = 0; i < 20; i++)
     6             {
     7                 try
     8                 {
     9                     Thread.sleep((long) (Math.random() * 1000));
    10                 }
    11                 catch (InterruptedException e)
    12                 {
    13                     e.printStackTrace();
    14                 }
    15 
    16                 System.out.println("hello: " + i);
    17             }
    18         }
    19 
    20     }
    21 
    22     public void execute2()
    23     {
    24         synchronized(Example.class)
    25         {
    26             for (int i = 0; i < 20; i++)
    27             {
    28                 try
    29                 {
    30                     Thread.sleep((long) (Math.random() * 1000));
    31                 }
    32                 catch (InterruptedException e)
    33                 {
    34                     e.printStackTrace();
    35                 }
    36 
    37                 System.out.println("world: " + i);
    38             }
    39         }    
    40     }
  • 相关阅读:
    (Java实现) 洛谷 P1106 删数问题
    (Java实现) 洛谷 P1603 斯诺登的密码
    (Java实现) 洛谷 P1036 选数
    (Java实现) 洛谷 P1012 拼数
    (Java实现) 洛谷 P1028 数的计算
    (Java实现) 洛谷 P1553 数字反转(升级版)
    (Java实现) 洛谷 P1051 谁拿了最多奖学金
    (Java实现) 洛谷 P1051 谁拿了最多奖学金
    (Java实现) 洛谷 P1106 删数问题
    目测ZIP的压缩率
  • 原文地址:https://www.cnblogs.com/2015110615L/p/6732695.html
Copyright © 2011-2022 走看看