java多线程-synchronized

大纲:

  1. 线程安全问题
  2. synchronized代码块
  3. 原理简述

 

一、线程安全问题

多线程操作各自线程创建的资源的时候,不存在线程安全问题。但多线程操作同一个资源的时候就会出现线程安全问题。下例为两个线程操作同一个name资源时发生的问题。

class TestSyn {

    public static void main(String[] args) throws Exception {
        Resource resource = new Resource();
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    r.sayb();
                }
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    r.sayq();
                }
            }
        }.start();
    }
}

class Resource {
    private String name;

    public void sayb() {
        name = "bbb";
        System.out.println(Thread.currentThread().getName() + name);//Thread-0打印bbb
    }

    public void sayq() {
        name = "qqqqqq";
        System.out.println(Thread.currentThread().getName() + name);//Thread-0打印bbb
    }
}
/**
 *
 Thread-1qqqqqq
 Thread-1qqqqqq
 Thread-0qqqqqq //其中一段错误信息,Thread-0线程也打印了qqqqqq
 Thread-0bbb
 Thread-0bbb
 */

问题出现过程:

  1. Thread-0获取执行权执行name="bbb"。
  2. Thread-1获得执行权执行name="qqqqqq"。
  3. Thread-0重新获得执行权执行打印指令,这时Thread-0就打印出了qqqqqq。

 

二、synchronized代码块

如果name赋值,打印name是一个原子操作就可以避免线程安全问题。

java中synchronized可以标记一段代码,达到原子操作的效果。

  1. 当一个线程执行标记有synchronized代码时将获得该对象的锁,然后开始执行synchronized标记的代码。
  2. 每一个对象只有一个锁,因此其他线程无法获得该对象锁。
  3. 其他线程如果这时候也执行到了标记有synchronized的代码将阻塞,直到获得对象锁的线程执行完synchronized标记的代码。
  4. 然后持有锁的线程释放锁。
  5. 其他线程开始争夺锁,回到第1步。

 synchronized标记代码有两种方式:

//synchronized代码块
class Resource {
    private String name;

    public void sayb() {
        synchronized (this){
            name = "bbb";
            System.out.println(Thread.currentThread().getName() + name);//Thread-0打印bbb
        }
        //...其他代码
    }

    public void sayq() {
        synchronized (this){
            name = "qqqqqq";
            System.out.println(Thread.currentThread().getName() + name);//Thread-0打印bbb
        }
        //...其他代码
    }
}
//synchronized方法
class Resource {
    private String name;

    public synchronized void sayb() {
        name = "bbb";
        System.out.println(Thread.currentThread().getName() + name);//Thread-0打印bbb
        //...其他代码
    }

    public synchronized void sayq() {
        name = "qqqqqq";
        System.out.println(Thread.currentThread().getName() + name);//Thread-0打印bbb
        //...其他代码
    }
}

上例中两个线程执行的是同一个对象的方法,因此他们抢夺同一个锁,一个线程执行的时候,另一个线程阻塞。

两种方法有些不同点:

  1. synchronized方法标记在非static方法上,线程获得的锁为this,例子中为resource对象。若标记在static方法上则线程获得的锁为Resource.class对象。
  2. synchronized标记在代码块上,可以由用户自己指定,而且代码块的范围也可以自己指定。因此synchronized代码块比synchronized方法更加灵活。

 

 三、原理简述

synchronized并非一开始就加上悲观锁,有一个锁升级的过程。无锁-->偏向锁-->轻量级锁-->重量级锁,锁可以升级不能降级,但可以撤销。

锁对象头中有mark word,倒数3位用于存放锁相关信息:倒数第3位标识是否偏向锁1是0否,倒数前2位标识01无锁/偏向锁(前面几位记录线程号);00轻量级锁(前面几位记录lock record地址);10重量级锁(前面几位记录monitor地址)。

偏向锁:当有线程第一次进入同步代码块,markword第三位置1,然后cas将线程号写入markword,偏向锁主要解决重入的性能问题,修改markword前检查是否和当前线程号一致。写入失败证明存在竞争锁升级。偏向锁存在偏向锁撤销,批量重偏向和批量撤销等问题。

轻量级锁:栈帧中开辟lock record空间记录锁对象地址并尝试cas将lock record地址和00标识位写入markword,写入失败锁会自旋(自旋现在都是自适应策略),自旋失败继续升级。

重量级锁:锁对象会关联一个monitor对象,mark word中存放monitor对象地址,monitor主要存放owner(当前持有锁的线程号),entryList(未竞争到锁block状态的线程),waitSet(存放wait状态的线程)。尝试修改owner失败的话,自旋,自旋失败进入entryList,等待锁释放被唤醒。

锁的释放:锁释放时如果是偏向锁标识,什么都不用做,如果是轻量级锁把最后2位置为01;如果是重量级锁,需要唤醒entryList中的线程。

posted @ 2018-12-07 17:26  扶不起的刘阿斗  阅读(409)  评论(0编辑  收藏  举报