[多线程]Java多线程03_线程同步及可见性

一、线程安全问题

线程安全问题是个经典的问题,主要涉及的方面就是对变量的修改以及读取。因为修改读取并不是一步完成,而计算机在切换线程的时候也没有清楚哪个时候可以切换,这时候就需要程序来给计算机指定道路说,你什么时候可以切换什么时候不行。特别是在对值修改的过程中,是不能够切换到读取线程的,要不然会出现脏读(即数据的错误)的问题。而Java对线程安全的控制主要是synchronized以及Lock锁。

关于变量安全控制的问题,我们现在大部分说到的变量安全问题均是类全局变量的安全问题,方法内部的局部变量,因为jvm在执行一个方法的时候都会进行线程栈之间的隔离,所以一个线程中对一个变量的修改创建并不会到另外一个线程对变量的修改创建。

(一)synchronized关键字

synchronized关键字主要用来修饰方法以及代码块,细节待会再说。被修饰的方法或者代码块只要有一个线程在执行,其他线程则必须等待该线程执行完毕,这就能够保证在赋值的时候,是赋值完成才切换给读取值的线程使用。并且可以通过Object类的wait()以及notify()方法进行线程之间的协调工作。但是细粒度并没有那么大,这也就是Lock类出现的理由。

(二)Lock锁

Lock值更能够细粒度的控制什么时候需要加锁,什么时候可以解锁,并且在线程之间通讯方面更加成熟,可以实现多个线程之间,指定线程的通讯。Lock锁放在后面才会说到

二、synchronized关键字

(一)基础用法和区别

示例代码

示例中一共有三个类:
– 图书类:代表图书,有一个方法setBookNameAndCode()是用来赋值的,也就是这个方法来测试线程安不安全
– 操作图书的业务层:用于创建不同的业务层给一个图书赋值
– 线程启动类:启动两个不同的业务层线程给一个共同的图书赋值

/**
 * 图书类
 * @author liweidan
 */
public class Book {
    /** 图书名字 */
    private String name;
    /** 图书编号 */
    private Integer bookCode;
    /**
     * 用于设置图书的名字和编号
     * @param name
     * @param code
     */
    public synchronized void setBookNameAndCode(String name, Integer code) {
        try {
            this.name = name;
            Thread.sleep(2000L);
            this.bookCode = code;
            System.out.println("bookName: " + this.name + ", code: " + this.bookCode);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    /** 省略getter setter toString */
}

/**
 * 图书业务类
 * @author liweidan
 * @date 2017.12.15 下午3:48
 * @email toweidan@126.com
 */
public class BookService implements Runnable {
    Book book;
    String name;
    Integer code;
    public BookService(Book book, String name, Integer code) {
        this.book = book;
        this.name = name;
        this.code = code;
    }
    @Override
    public void run() {
        book.setBookNameAndCode(name, code);
    }
}

public static void testSync01() {
    /** 创建一个共享资源 */
    Book book = new Book();
    /** 创建两个线程来对这个共享资源进行修改 */
    Thread t1 = new Thread(new BookService(book, "Java多线程", 10001), "Thread01");
    Thread t2 = new Thread(new BookService(book, "JVM虚拟机", 10002), "Thread02");
    t1.start();
    t2.start();

}

结果:

一共有两个结果:

    /**
     没有加synchronized:
        bookName: JVM虚拟机, code: 10001
        bookName: JVM虚拟机, code: 10001
     加synchronized:
         bookName: Java多线程, code: 10001
         bookName: JVM虚拟机, code: 10002
     */

1.没有加synchronized的情况

$seq
Thread01->共同的图书: this.name = “Java多线程”
Note right of Thread01:Thread01开始sleep
Thread02->共同的图书: this.name = “JVM虚拟机”
Note left of Thread02:Thread02开始sleep
Thread02->共同的图书: this.code = “10002”(Thread02抢到了CPU,先给赋值)
Thread01->共同的图书: this.code = “10001”
Thread01->共同的图书: 打印bookName: JVM虚拟机, code: 10001
Thread02->共同的图书: 打印bookName: JVM虚拟机, code: 10001
$

2.有加synchronized的情况

$seq
Thread01->共同的图书: 获得线程锁
Thread01->共同的图书: this.name = “Java多线程”
Note right of Thread01:Thread01开始sleep
Thread01->共同的图书: this.code = “10001”
Thread01->共同的图书: 打印bookName: Java多线程, code: 10001
Thread01->共同的图书: 释放线程锁
Thread02->共同的图书: 获得线程锁
Thread02->共同的图书: this.name = “JVM虚拟机”
Note left of Thread02:Thread02开始sleep
Thread02->共同的图书: this.code = “10002”
Thread02->共同的图书: 打印bookName: JVM虚拟机, code: 10002
Thread02->共同的图书: 释放线程锁
$

总结

可见,在需要同步的方法上加上synchronized关键字,可以起到让jvm按照顺序来执行的效果。当有多个线程进行访问的时候,这时候只能“排队”一个线程一个线程来执行方法,从而让方法安全。

另外一个重点是,在示例中一直强调一个资源,因为只有多个线程对同一个资源进行操作的时候才会出现线程安全问题,当我们创建两个资源,有两个线程分别操作的时候,根本就不会出现线程安全问题,synchronized关键字也不会让方法同步执行,因为不同的对象,是不同的锁,这时候操作依然会是异步操作。

(二)脏读问题

如果set方法同步了,但是get方法没有进行同步,那么get方法是会异步请求的。

执行过程:

$seq
设值线程->共同的图书: 获得线程锁
设值线程->共同的图书: this.name = “Java多线程”
Note right of 设值线程: 设值线程开始sleep
共同的图书->取值线程: 打印bookName: Java多线程, code:null
Note left of 取值线程: 因为取值没有同步所以进来了
设值线程->共同的图书: this.code = “10001”
设值线程->共同的图书: 释放线程锁
$

初步解决方式:给取值方法也加上synchronized,但是会引起性能问题。

这里有两个知识点:
1. 当一个同步方法被调用的时候,其他线程必须等待这个线程释放锁的时候才可以执行该方法或者其他synchronized方法
2. 当一个同步方法被调用的时候,另外一个方法如果没有同步,依然可以被异步调用

(三)锁重入

意思是:如果一个方法调用另外一个同步方法的时候,会立即获得锁,并且等这个方法执行完成以后才释放。

示例

修改setBookNameAndCode让他调用另外一个同步方法:

/**
 * 图书类
 * @author liweidan
 */
public class Book {
    /** 图书名字 */
    private String name;
    /** 图书编号 */
    private Integer bookCode;
    /**
     * 用于设置图书的名字和编号
     * @param name
     * @param code
     */
    public synchronized void setBookNameAndCode(String name, Integer code) {
        try {
            this.name = name;
            Thread.sleep(2000L);
            this.bookCode = code;
            anohtherMethod();
            System.out.println("bookName: " + this.name + ", code: " + this.bookCode);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void anohtherMethod() {
        System.out.println("============ anohtherMethod ===========");
    }
}

结果

============ anohtherMethod ===========
bookName: Java多线程, code: 10001
============ anohtherMethod ===========
bookName: JVM虚拟机, code: 10002

(四)锁无继承性

当父类一个同步方法被子类重写的时候,依然需要加上同步标记

(五)总结

  1. 当方法加上synchronized关键字的时候,线程需要排队执行这个方法
  2. 当A线程调用一个同步方法的时候,B线程可以调用其他的非同步的方法
  3. 如果有两个同步方法,A线程在执行第一个方法的时候,B线程需要等待A线程的锁才可以进行执行另外一个同步方法
  4. 锁重入,当一个同步方法执行另外一个同步方法的时候,该线程可以立即获得锁继续执行
  5. 无继承性:子类重写父类同步方法的时候,需要再加上synchronized关键字

三、synchronized修饰代码块

(一)锁是对象自己的时候

synchronized方法的弊端:锁住整个方法,当方法中有些操作不需要同步的时候,也一起同步了,造成了不必要的性能损失。

synchronized方法的锁对象也是本对象,所以当我们用代码块锁锁住this的时候,其实性能是基本没有差别的。

示例

简单小示例来做一下,还是上面的例子,不过通过模拟耗时的不需要同步的操作来模仿后续

/**
 * 使用对象锁锁代码块
 * @author liweidan
 * @date 2017.12.15 下午5:15
 * @email toweidan@126.com
 */
public class BookObjLock {
    /** 图书名字 */
    private String name;
    /** 图书编号 */
    private Integer bookCode;
    /**
     * 用于设置图书的名字和编号
     * @param name
     * @param code
     */
    public void setBookNameAndCode(String name, Integer code) {
        int aNum = 0;
        for (int i = 0; i < 100000; i++) {
            // 其他事情
            aNum += i;
        }
        System.out.println(Thread.currentThread().getName() + ": " + aNum);
        synchronized (this) {
            try {
                this.name = name;
                Thread.sleep(2000L);
                this.bookCode = code;
                System.out.println("bookName: " + this.name + ", code: " + this.bookCode);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 图书业务类
 * @author liweidan
 * @date 2017.12.15 下午3:48
 * @email toweidan@126.com
 */
public class BookObjLockService implements Runnable {
    BookObjLock book;
    String name;
    Integer code;
    public BookObjLockService(BookObjLock book, String name, Integer code) {
        this.book = book;
        this.name = name;
        this.code = code;
    }
    @Override
    public void run() {
        book.setBookNameAndCode(name, code);
    }
}

public static void testObjLock() {
    /** 创建一个共享资源 */
    BookObjLock book = new BookObjLock();
    /** 创建两个线程来对这个共享资源进行修改 */
    Thread t1 = new Thread(new BookObjLockService(book, "Java多线程", 10001), "Thread01");
    Thread t2 = new Thread(new BookObjLockService(book, "JVM虚拟机", 10002), "Thread02");
    t1.start();
    t2.start();
}

结果

/** 这里是长操作,无同步 */
Thread01: 704982704
Thread02: 704982704
bookName: Java多线程, code: 10001
bookName: JVM虚拟机, code: 10002

可以看到,需要长时间的操作并不会同步执行。这样子就能大大提高项目的性能。

(二)锁是其他对象

代码块中,锁可以设置为其他对象,这样子就可以更进一步控制同步的时机。

示例

对设置namecode的方法加上不同的锁,以便在设置书名或者书编号的时候可以不被干扰。

public class BookObjLock {
    /** 图书名字 */
    private String name;
    /** 图书编号 */
    private Integer bookCode;
    /**
     * 用于设置图书的名字和编号
     * @param name
     * @param code
     */
    public void setBookNameAndCode(String name, Integer code) {
        int aNum = 0;
        for (int i = 0; i < 100000; i++) {
            // 其他事情
            aNum += i;
        }
        System.out.println(Thread.currentThread().getName() + ": " + aNum);
        synchronized (this) {
            try {
                this.name = name;
                Thread.sleep(2000L);
                this.bookCode = code;
                System.out.println("bookName: " + this.name + ", code: " + this.bookCode);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public Integer getBookCode() {
        return bookCode;
    }
    public void setBookCode(Integer bookCode) {
        synchronized ("code") {
            this.bookCode = bookCode;
            System.out.println("setBookCode============= name: " + this.name + ", code: "+this.bookCode + ", current" + System.currentTimeMillis());
            System.out.println(Thread.currentThread().getName() + "setBookCode over");
        }
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        synchronized ("name") {
            this.name = name;
            System.out.println("setName============= name: " + this.name + ", code: "+this.bookCode + ", current" + System.currentTimeMillis());
            System.out.println(Thread.currentThread().getName() + "setName over");
        }
    }
    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", bookCode=" + bookCode +
                '}';
    }
}

/** 两个线程 */
/**
 * 
 * @author liweidan
 * @date 2017.12.15 下午5:45
 * @email toweidan@126.com
 */
public class BookNameThread implements Runnable {
    BookObjLock book;
    String name;
    public BookNameThread(BookObjLock book, String name) {
        this.book = book;
        this.name = name;
    }
    @Override
    public void run() {
        book.setName(name);
    }
}

/**
 * 
 * @author liweidan
 * @date 2017.12.15 下午5:45
 * @email toweidan@126.com
 */
public class BookCodeThread implements Runnable {
    BookObjLock book;
    Integer code;
    public BookCodeThread(BookObjLock book, Integer code) {
        this.book = book;
        this.code = code;
    }
    @Override
    public void run() {
        book.setBookCode(code);
    }
}

public static void testMultiLock() {
    /** 创建一个共享资源 */
    BookObjLock book = new BookObjLock();
    Thread t1 = new Thread(new BookCodeThread(book, 10001), "t1-setCode");
    Thread t2 = new Thread(new BookCodeThread(book, 10002), "t2-setCode");
    Thread t3 = new Thread(new BookNameThread(book, "JVM虚拟机"), "t3-setName");
    t1.start();
    t2.start();
    t3.start();
}

结果

setBookCode============= name: null, code: 10001, current1513331932626
setName============= name: JVM虚拟机, code: 10001, current1513331932626
t1-setCodesetBookCode over
t3-setNamesetName over
setBookCode============= name: JVM虚拟机, code: 10002, current1513331932626
t2-setCodesetBookCode over

可以看到,设置code方法和设置name方法是异步执行的,只有是当两个线程抢到同一个锁的时候,才会同步执行。但是如果锁不是同一个,那么会是异步运行。

换句话说,通过控制不同方法锁不同的对象,可以控制线程执行不同方法的同步性。

注意:由于String对象具有一旦创建就放入常量池的特性,所以在加锁的时候,应该避免使用String作为线程锁,取而代之的是使用new Object()作为线程锁,并且这个对象不放入缓存中(即为了避免是同一个对象而让我们误以为是不同对象造成业务出错)

(三)嵌套锁与死锁

嵌套锁:就是一个同步代码块中,又有另外一个代码块。多用于一个方法修改两个对象,需要锁住不同对象的时候,但是其实开发中比较少遇到这种情况。

死锁:嵌套锁情况下,两个锁出现了互相等待,造成程序卡主无法执行下去。

示例

直接使用死锁来看嵌套锁的例子。

/**
 * 演示死锁
 * @author liweidan
 * @date 2017.12.16 下午2:35
 * @email toweidan@126.com
 */
public class DeadLine implements Runnable {
    private String userName;
    private Object lock1 = new Object();
    private Object lock2 = new Object();
    public void setUserName(String userName) {
        this.userName = userName;
    }
    @Override
    public void run() {
        if (userName.equals("a")) {
            synchronized (lock1) {
                doSomething();
                synchronized (lock2) {
                    System.out.println("Lock1 -> Lock2");
                }
            }
        } else if (userName.equals("b")) {
            synchronized (lock2) {
                doSomething();
                synchronized (lock1) {
                    System.out.println("Lock2 -> Lock1");
                }
            }
        }
    }
    private void doSomething() {
        try {
            System.out.println("username = " + userName);
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public static void testDeadlock() {
    try {
        DeadLine deadLine = new DeadLine();
        deadLine.setUserName("a");
        Thread t1 = new Thread(deadLine);
        t1.start();
        Thread.sleep(100);
        deadLine.setUserName("b");
        Thread t2 = new Thread(deadLine);
        t2.start();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

结果:

username = a
username = b

通过结果可以看到,两个线程卡住了,而程序并未运行结束,一直跑着。

执行时序图

$seq
线程t1->DeadLine: 创建了A的userName并且执行
线程t1->DeadLine: 获得Lock1
线程t1->DeadLine: 线程在run方法内睡眠3秒
线程2->DeadLine: 300秒被创建,B的userName并且执行
线程2->DeadLine: 获得Lock2
线程2->DeadLine: 线程在run方法内睡眠3秒
线程t1->DeadLine: 醒来,等待Lock2才能继续执行
线程2->DeadLine: 醒来,等待Lock1才能继续执行
Note right of DeadLine: 正是这一步导致死锁
$

四、volatile关键字

(一)概念

volatile修饰了变量后,当有多个线程拿到这个变量的时候,能够拿到最新的值。

jvm有主内存、线程栈的概念,当线程A改变了变量的值的时候,而线程B读取该值的时候,并不能够马上读到新的值,因为线程A改变的值并不会相应到线程B的线程栈中,要解决这个问题,加上volatile后,每次线程B取值的时候都会从公共堆中去取值。

接下来使用程序以及时序图来帮助理解

(二)具体示例

1. 未加上volatile

/**
 * @author liweidan
 * @date 2017.12.16 下午3:48
 * @email toweidan@126.com
 */
public class VolatileThread implements Runnable {
    private boolean isRunning = true;
    public void changeRunning() {
        isRunning = !isRunning;
    }
    public boolean isRunning() {
        return isRunning;
    }
    @Override
    public void run() {
        System.out.println("============== start ==============");
        while (isRunning) {
        }
        System.out.println("================= End =============");
    }
    public static void main(String[] args) {
        try {
            /** 创建一个资源 */
            VolatileThread thread = new VolatileThread();
            /** 启动一个线程执行run方法 */
            Thread t1 = new Thread(thread);
            t1.start();
            /** 修改变量的值 */
            Thread.sleep(3000L);
            thread.changeRunning();
            System.out.println(thread.isRunning);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果如下:

============== start ==============
false

2. 加上volatile

public class VolatileThread implements Runnable {
    private volatile boolean isRunning = true;
    .......
}

结果如下:

============== start ==============
false
================= End =============

可以看出来:
– 未加上volatile的时候,虽然主线程在3秒以后把isRunning进行了修改,但是线程停不下来。
– 加上volatile的时候,主线程在3秒以后把isRunning进行了修改,线程停下来了。

3. 分析结果差异

老方法,时序图分析过程。

对于未加上volatile的时候,程序是这么执行的:

$seq
线程t1->公共堆: 从主内存中读取isRunning变量
线程t1->线程栈: 写入isRunning=true
线程t1->线程栈: isRunning=true,写到线程栈,while循环
线程Main->公共堆: 等待3秒后,把isRunning改为false
线程Main->公共堆: 输出isRunning=false,结束
线程t1->线程栈: 读取线程栈isRunning=true,while一直循环
$

加上volatile的时候,程序是这么执行的:

$seq
线程t1->公共堆: 从主内存中读取isRunning变量
线程t1->线程栈: 读取isRunning=true,while开始循环
线程Main->公共堆: 等待3秒后,把isRunning改为false
线程Main->公共堆: 输出isRunning=false,结束
线程t1->公共堆: 读取线程栈isRunning= false,while停止
$

可以看出,当变量加上volatile的时候,那么强制线程栈每次读取变量的时候都从公共堆里面去读,所以能够读到最新的值。

(三)非原子性

1. 原子性问题

/**
 * 测试非原子性
 * @author liweidan
 * @date 2017.12.16 下午4:24
 * @email toweidan@126.com
 */
public class VolatileThread02 implements Runnable {
    private static int count;
    private static void addCount() {
        for (int i = 0; i < 100; i++) {
            count++;
        }
        System.out.println(count);
    }
    @Override
    public void run() {
        addCount();
    }
    public static void main(String[] args) {
        VolatileThread02[] ts = new VolatileThread02[100];
        Thread[] threads = new Thread[100];
        for (int i = 0; i < 100; i++) {
            ts[i] = new VolatileThread02();
            threads[i] = new Thread(ts[i]);
        }
        for (int i = 0; i < 100; i++) {
            threads[i].start();
        }
    }
}

/**  结果: */
....
9493
9593
9693
9793
9893
9993

结果并没有如我们所愿,一百一百的增加。所以我们需要给addCount()方法加上synchronized关键字才能让变量变成原子性的增加。

所以关键字volatile并不是说可以保证变量的安全,如需保证安全性依然需要使用synchronized关键字,关键字volatile只是为了保证变量在被取值的时候,都是最新的状态。

2. i++问题

上面讲到i++需要三个步骤:
a. 读取i的值
b. 新增
c. 写入主存

但是如果加上volatile关键字,是可以保证第a步,但是如果此时又被其他线程读取执行了第a步,那么就会出现脏数据

原子类AtomicXXX

AtomicXXX是类似于i++操作的原子类,后面的XXX可以替换成Java包装类的名字。

/**
 * 原子类Demo
 * @author liweidan
 * @date 2017.12.16 下午4:38
 * @email toweidan@126.com
 */
public class AtomicDemo extends Thread {
    private AtomicInteger count = new AtomicInteger(0);
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            /** 新增并且获取 */
            System.out.println(count.incrementAndGet());
        }
    }
    public static void main(String[] args) {
        AtomicDemo atomicDemo = new AtomicDemo();
        Thread t1 = new Thread(atomicDemo);
        Thread t2 = new Thread(atomicDemo);
        Thread t3 = new Thread(atomicDemo);
        Thread t4 = new Thread(atomicDemo);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

...
39994
39995
39996
39997
39998
39999
40000

结果可以安全的新增到40000

注意:虽然原子类是安全的,但是需要方法调用之间的安全性。

3. synchronized可以具备可见性

synchronized关键字可以是多线程访问同一个变量具有同步性,而且还具备将工作内存中的变量与公共内存中的变量同步的功能。

五、总结

(一)synchronized:

  1. 修饰方法可以让方法执行具有同步性,多个线程时需要依次逐个进入执行
  2. 修饰代码块,指定对象锁,当线程获取进入代码块的时候,其他线程需要等待进入
  3. 修饰代码块,指定多个对象锁,当线程获取进入代码块的时候,其他线程如需要进入同个代码块里,需要等待,但是如果进入的是另外一个锁的代码块,则可以和其他线程异步进行
  4. 脏读:当设值的时候进行了同步,但是可能设值和读取值的线程异步进行导致获取值是脏的数据。解决:给取值方法加上同步
  5. i++的原子性:读取、操作、赋值
  6. 锁重入:当一个加锁的方法或者代码块调用另外一个加锁的方法,可以立即获取锁
  7. 无继承性:重写父类同步方法需要重新声明

(二)volatile:

  1. 内存间的可见性
  2. 非原子性
点赞