• synchronized


     线程安全是并行程序的根本和根基。一般来说,程序的并行化是为了获取更高的执行效率,但前提是,高效率不能以牺牲正确为代价。

    先看下面这种情况:

     1 public class AccountingVol implements Runnable {
     2 
     3     static volatile int i = 0;
     4     @Override
     5     public void run() {
     6         for (int j = 0;j < 10000;j++){
     7             i++;
     8         }
     9     }
    10     //测试
    11     public static void main(String[] args) throws InterruptedException {
    12         Thread t1 = new Thread(new AccountingVol());
    13         Thread t2 = new Thread(new AccountingVol());
    14         t1.start();
    15         t2.start();
    16         t1.join();
    17         t2.join();
    18         System.out.println(i);
    19     }
    20 }

      上述代码运行多次发现,输出都比20000小,我们用两个线程去各自累加10000次。这就是多线程中的线程不安全问题。

    分析一下产生这种情况的原因:

      两个线程同时读取变量i为0,并各自计算得到 i= 1,并先后写入结果,这样,虽然i++被执行了两次,但是实际上i的值只增加了1。

    想要解决这个问题,我们就必须保证多个线程在对i 操作时完全同步,也就是说,当线程A在写入时,线程B不仅不能写,读都不可以,因为在A写完前,线程B读到的数据一定是过期的数据。在Java中,synchronized 可以解决这个问题。

    synchronized的作用

      是实现线程间的同步,它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步区域,从而保证线程的安全性,换句话说,被synchronized限制的多线程块里的代码是串行执行的。

    synchronized的用法:

    按加锁对象分为:

      ① 给对象加锁:对指定的对象加锁,进入同步代码前要获得给定对象的锁。

      ②对实例加锁:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁;注意:只要对象的引用不变,即使对象的属性改变,运行的结果依然是同步的。

      ③对静态方法加锁:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

      ④对任意对象加锁:比如synchronized(String),大多数都是方法的参数,或者是一个类的全局变量等。

    下面就是对上面例子的修正:

     1 public class AccountingVol implements Runnable {
     2 
     3     static volatile int i = 0;
     4     @Override
     5     public void run() {
     6         for (int j = 0;j < 10000;j++){
     7             synchronized (this){
     8                   i++;
     9             }
    10         }
    11     }
    12     //测试
    13     public static void main(String[] args) throws InterruptedException {
    14         AccountingVol accountingVol = new AccountingVol();
    15         Thread t1 = new Thread(accountingVol);
    16         Thread t2 = new Thread(accountingVol);
    17         t1.start();
    18         t2.start();
    19         t1.join();
    20         t2.join();
    21         System.out.println(i);
    22     }
    23 }

    输出结果:

    1 20000

    上述代码就是对当前对象加锁,this指当前对象。从输出结果来看,表明同步成功。

    那在看看下面的例子:

     1 public class AccountingVol implements Runnable {
     2 
     3     static volatile int i = 0;
     4     @Override
     5     public void run() {
     6         for (int j = 0;j < 10000;j++){
     7             synchronized (this){
     8                   i++;
     9             }
    10         }
    11     }
    12     //测试
    13     public static void main(String[] args) throws InterruptedException {
    14         Thread t1 = new Thread(new AccountingVol());
    15         Thread t2 = new Thread(new AccountingVol());
    16         t1.start();
    17         t2.start();
    18         t1.join();
    19         t2.join();
    20         System.out.println(i);
    21     }
    22 }

    执行后,你发现值又小于2000了。我也加了synchronized了,怎么还会出现线程不安全呢?

    虽然是同步了当前对象,但是t1和t2 是两个不同的对象,代码第14,15行,相当于同步的只是自己的实例,换句话说,这两个线程用了两把不同的锁,因此,线程安全无法保证。

    synchronized其它特性:

      synchronized除了用于保证线程安全和线程同步之外,还可以保证线程间的可见性和有序性。从可见性的角度讲,synchronized完全可以代替volatile的功能,只是使用上没有那么方便。就有序性而言,由于synchronized限制每次只有一个线程可以访问同步代码,因此,无论同步的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的。而其他线程,又必须获得锁后才能进入同步代码读取数据,因此,它们看到的最终结果并不取决于代码的执行过程,从而有序性问题得到解决。

    synchronized的使用技巧

      用关键字synchronized声明方法在某些情况下是有弊端的,比如线程A调用同步方法执行一个时间较长的任务,那么其他的线程线程就必须等待较长时间,对于这种情况,我们可以采用同步代码块来缩小同步的区域,这样可以提高代码的执行效率。下面举例说明:

     1 public class LongTimeTask implements Runnable {
     2     @Override
     3     public void run() {
     4         //没有同步的代码
     5         for (int i = 0;i < 100;i++){
     6             System.out.println("No synchronized ThreadName:" + Thread.currentThread().getName() + ", i = " + i);
     7         }
     8 
     9         //同步代码
    10         synchronized (this){
    11             for (int i = 0;i < 100;i++){
    12                 System.out.println("Synchronized ThreadName:" + Thread.currentThread().getName() + ", i = " + i);
    13             }
    14         }
    15     }
    16     //测试
    17     public static void main(String[] args) throws InterruptedException {
    18         LongTimeTask longTimeTask = new LongTimeTask();
    19         Thread t1 = new Thread(longTimeTask,"t1");
    20         Thread t2 = new Thread(longTimeTask,"t2");
    21         t1.start();
    22         t2.start();
    23         t1.sleep(1000);
    24     }
    25 }

    输出结果:

    第一部分:

    ........
    Synchronized ThreadName:t1, i = 30
    No synchronized ThreadName:t2, i = 0
    Synchronized ThreadName:t1, i = 31
    No synchronized ThreadName:t2, i = 1
    Synchronized ThreadName:t1, i = 32
    No synchronized ThreadName:t2, i = 2
    Synchronized ThreadName:t1, i = 33
    No synchronized ThreadName:t2, i = 3
    Synchronized ThreadName:t1, i = 34
    No synchronized ThreadName:t2, i = 4
    Synchronized ThreadName:t1, i = 35
    No synchronized ThreadName:t2, i = 5
    Synchronized ThreadName:t1, i = 36
    No synchronized ThreadName:t2, i = 6
    Synchronized ThreadName:t1, i = 37
    No synchronized ThreadName:t2, i = 7
    Synchronized ThreadName:t1, i = 38
    No synchronized ThreadName:t2, i = 8
    ........

    第二部分:

    ........
    Synchronized ThreadName:t1, i = 97
    Synchronized ThreadName:t1, i = 98
    Synchronized ThreadName:t1, i = 99
    ........
    Synchronized ThreadName:t2, i = 0
    Synchronized ThreadName:t2, i = 1
    Synchronized ThreadName:t2, i = 2
    Synchronized ThreadName:t2, i = 3
    ........

    由第一个输出结果可以看出:当A线程访问synchronized同步代码块的时候,B线程依然可以访问对象方法中其余非synchronized代码块的部分。

    由第二个输出结果可以看出:当线程A访问synchronized同步代码块的时候,线程B也想要访问这部分代码时,必须等到线程A访问完之后。

    还有一个结论:两个synchronized块之间具有互斥性,就是线程A在访问对象中的一块synchronized代码块时,线程B想要访问这个对象中的另一块synchronized代码块,这是线程B会被阻塞,因为线程B执行前回去获取这个对象,但是这个对象锁却在线程A中。

    对任意对象加锁

     例子: 

     1 public class Anything implements Runnable {
     2 
     3     private String anything = new String();
     4 
     5     @Override
     6     public void run() {
     7         synchronized (anything){
     8             System.out.println("线程名称为:" + Thread.currentThread().getName() +
     9                     "在 " + System.currentTimeMillis() + " 进入同步代码块");
    10             try {
    11                 Thread.sleep(3000);
    12             } catch (InterruptedException e) {
    13                 e.printStackTrace();
    14             }
    15             System.out.println("线程名称为:" + Thread.currentThread().getName() +
    16                     "在 " + System.currentTimeMillis() + " 离开同步代码块");
    17         }
    18     }
    19     //测试
    20     public static void main(String[] args) throws InterruptedException {
    21         Anything a = new Anything();
    22         Thread t1 = new Thread(a);
    23         Thread t2 = new Thread(a);
    24         t1.start();
    25         t2.start();
    26     }
    27 }

    输出:

    线程名称为:Thread-0在 1537430191272 进入同步代码块
    线程名称为:Thread-0在 1537430194272 离开同步代码块
    线程名称为:Thread-1在 1537430194272 进入同步代码块
    线程名称为:Thread-1在 1537430197272 离开同步代码块

     由输出可以看出,同步是成功的。解释一下:代码的第 3 行,将anything定义为了全局变量,因此拿到的锁对象相当于是类对象,自然就是同步的结果;如果把第3行代码,放到run()方法里面去,那么两个线程监视的就不是同一个变量了,就是两个不同的变量,这样同步就会失败。

      锁对象如果是任意对象(除了this对象)具有一定的优势:如果类中有很多的synchronized方法,这时虽然能实现同步,但是阻塞严重效率较低但是如果同步锁是非this对象,那么synchronized(非this对象)与对象锁同步方法是异步的,不是阻塞的,这样就提高了运行效率。

    synchronized是重入锁

    看下面的例子:

     1 public class synchronizedDemo implements Runnable{
     2 
     3    @Override
     4    public void run() {
     5        //  第一次获得锁
     6        synchronized (this) {
     7            while (true) {
     8                //  第二次获得同样的锁
     9                synchronized (this) {
    10                    System.out.println("ReteenLock!");
    11                }
    12                try {
    13                    Thread.sleep(1000);
    14                } catch (InterruptedException e) {
    15                    e.printStackTrace();
    16                }
    17            }
    18        }
    19    }
    20    //测试
    21    public static void main(String[] args){
    22        Thread thread = new Thread(new synchronizedDemo());
    23        thread.start();
    24    }
    25 }

     输出:

    1 ReteenLock!
    2 ReteenLock!
    3 ReteenLock!
    4 ReteenLock!
    5 ReteenLock!
    6 ReteenLock!
    7 .......

    一目了然,如果synchronized不是可重入锁,则在第二次获取锁时,会产生死锁,但是运行并且有结果没有报错,则证明synchronized是可重入锁。

     synchronized继承属性

     1 public class Father {
     2 
     3
     4     public synchronized void subOpt() throws InterruptedException {
     5         System.out.println("Father 线程进入时间:" + System.currentTimeMillis());
     6         Thread.sleep(5000);
     7         System.out.println("Father!" + Thread.currentThread().getName());
     8     }
     9 }
    10 
    11 //重写父类方法
    12 class SonOverRide extends Father{
    13     @Override
    14     public void subOpt() throws InterruptedException {
    15         System.out.println("SonOverRide 线程进入时间:" + System.currentTimeMillis());
    16         Thread.sleep(3000);
    17         System.out.println("SonOverRide!" + Thread.currentThread().getName());
    18     }
    19 }
    20 //不重写父类方法
    21 class Son extends Father{
    22 
    23 }
    24 //测试类
    25 class Test{
    26     public static void main(String[] args){
    27         //测试重写父类中方法类
    28         SonOverRide sonOverRide = new SonOverRide();
    29         for (int i= 0;i < 5;i++){
    30             new Thread(){
    31                 @Override
    32                 public void run() {
    33                     try {
    34                         sonOverRide.subOpt();
    35                     } catch (InterruptedException e) {
    36                         e.printStackTrace();
    37                     }
    38                 }
    39             }.start();
    40         }
    41         //测试未重写父类方法的类
    42         Son son = new Son();
    43         for (int i = 0;i < 5;i++){
    44             new Thread(){
    45                 @Override
    46                 public void run() {
    47                     try {
    48                         son.subOpt();
    49                     } catch (InterruptedException e) {
    50                         e.printStackTrace();
    51                     }
    52                 }
    53             }.start();
    54         }
    55     }
    56 }

     输出结果:

     1 SonOverRide 线程进入时间:1537494269453
     2 SonOverRide 线程进入时间:1537494269453
     3 SonOverRide 线程进入时间:1537494269453
     4 SonOverRide 线程进入时间:1537494269453
     5 SonOverRide 线程进入时间:1537494269454
     6 Father 线程进入时间:1537494269455
     7 SonOverRide!Thread-0
     8 SonOverRide!Thread-1
     9 SonOverRide!Thread-3
    10 SonOverRide!Thread-2
    11 SonOverRide!Thread-4
    12 Father!Thread-5
    13 Father 线程进入时间:1537494274455
    14 Father!Thread-6
    15 Father 线程进入时间:1537494279455
    16 Father!Thread-7
    17 Father 线程进入时间:1537494284456
    18 Father!Thread-9
    19 Father 线程进入时间:1537494289456
    20 Father!Thread-8

     观察线程进入时间,可以看出重写父类的方法并且没有加上关键字synchronized时,没有同步效果,线程进入时间几乎为同一时间;没有重写父类方法,有同步效果,上一个线程进入到下一个线程进入,间隔刚好5S。

    将未重写父类的方法修改为下面这样:

    1 class Son extends Father{
    2     public void subOpt() throws InterruptedException {
    3         super.subOpt();
    4         System.out.println("Son 线程进入时间:" + System.currentTimeMillis());
    5         Thread.sleep(3000);
    6         System.out.println("Son!" + Thread.currentThread().getName());
    7     }
    8 }

     再次运行结果:

     1 SonOverRide 线程进入时间:1537496547649
     2 SonOverRide 线程进入时间:1537496547649
     3 SonOverRide 线程进入时间:1537496547651
     4 SonOverRide 线程进入时间:1537496547651
     5 SonOverRide 线程进入时间:1537496547653
     6 Father 线程进入时间:1537496547653
     7 SonOverRide!Thread-0
     8 SonOverRide!Thread-3
     9 SonOverRide!Thread-1
    10 SonOverRide!Thread-2
    11 SonOverRide!Thread-4
    12 Father!Thread-6
    13 Son 线程进入时间:1537496552653
    14 Father 线程进入时间:1537496552653
    15 Son!Thread-6
    16 Father!Thread-7
    17 Son 线程进入时间:1537496557654
    18 Father 线程进入时间:1537496557654
    19 Son!Thread-7
    20 Father!Thread-9
    21 Son 线程进入时间:1537496562654
    22 Father 线程进入时间:1537496562654
    23 Son!Thread-9
    24 Father!Thread-5
    25 Son 线程进入时间:1537496567654
    26 Father 线程进入时间:1537496567654
    27 Son!Thread-5
    28 Father!Thread-8
    29 Son 线程进入时间:1537496572654
    30 Son!Thread-8

    观察输出结果第6,14,18,22,26行,发现调用父类的方法(super.subOpt()),是同步的,每一次调用都是相差5S,看输出结果第13,14,15行,可以看出在执行完父类的同步方法后,子类的方法内就不同步了,因为还没有打印出 Son!Thread-6 下一个线程就已经进入方法了。

    原因:由于调用了父类中的方法,父类方法是同步的,所以每个线程进入时都需要获取父类对象锁,由于前一个线程未执行完,没有释放父类对象锁,下一个线程就必须等待上一个线程释放后才能进入同步方法,子类中如果增加了自己的方法体,那么这一部分就不是同步的,属于子类的,子类的方法不是同步的,自然就不同步。但是如果子类只是调用了父类的方法,则肯定是同步的,相当于把子类的方法内加了一个同步代码块,代码块里包含了所有的子类方法体,自然就是同步的,如下所示:

    1 class Son extends Father{
    2     public void subOpt() throws InterruptedException {
    3         super.subOpt();
    4     }
    5 }
    作者:Joe
    努力了的才叫梦想,不努力的就是空想,努力并且坚持下去,毕竟这是我相信的力量
  • 相关阅读:
    跨站脚本攻击
    性能测试总述
    转:KPI(Key Performance Indicator,关键绩效指标)
    《C#与.NET3.5高级程序设计(第4版)》笔记2
    三层架构的一点小小的认识(很小很小)
    《C#与.NET3.5高级程序设计(第4版)》笔记3
    windows 2008无线网络"没有正确配置为使用IP协议" 错误解决办法
    《C#与.NET3.5高级程序设计(第4版)》笔记6
    A+=B和A=A+B真的有区别吗
    《C#与.NET3.5高级程序设计(第4版)》笔记5
  • 原文地址:https://www.cnblogs.com/Joe-Go/p/9679178.html
Copyright © 2020-2023  润新知