線程安全需要保證幾個基本特性:
- 原子性:相關操作不會中途被其他線程干擾,一般通過同步實現
- 可見性:一個線程修改了某個共享變量,其狀態(tài)能夠立即被其它線程知曉
- 有序性:保證線程內串行語義,避免指令重排等
1. synchronized
1.1 sychronized(class)代碼塊與靜態(tài)同步synchronized方法
兩者鎖定的都是對應的class,在效果上是等價的讥电,sychronized(class)代碼塊的粒度會小一些
public class SynchronizedTask {
// sychronized(class)代碼塊
public static void printA() {
synchronized (SynchronizedTask.class) {
try{
System.out.println("線程" + Thread.currentThread().getName() + "開始進入方法printA, 時間:" + System.currentTimeMillis());
Thread.currentThread().sleep(3000);
System.out.println("線程" + Thread.currentThread().getName() + "開始退出方法printA, 時間:" + System.currentTimeMillis());
}catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
// 靜態(tài)同步synchronized方法
synchronized public static void printB() {
try{
System.out.println("線程" + Thread.currentThread().getName() + "開始進入方法printB, 時間:" + System.currentTimeMillis());
Thread.currentThread().sleep(3000);
System.out.println("線程" + Thread.currentThread().getName() + "開始退出方法printB, 時間:" + System.currentTimeMillis());
}catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
public class ThreadA extends Thread {
@Override
public void run() {
SynchronizedTask.printA();
}
}
public class ThreadB extends Thread {
@Override
public void run() {
SynchronizedTask.printB();
}
}
public class SynchronizedMain {
public static void main(String[] args) {
ThreadA threadA = new ThreadA();
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB();
threadB.setName("B");
threadB.start();
}
}
由于線程A先持有class鎖,因此線程A執(zhí)行完printA方法后,線程B才獲得class鎖馋贤,執(zhí)行printB方法。1.2 synchronized方法和sychronized(this)代碼塊
兩者鎖定的都是當前對象畏陕,在效果上是等價的配乓,sychronized(this)代碼塊的粒度會小一些
package Synchronized.synchronized_current_object;
/**
* Created by xq on 2018/7/5.
*/
public class SynchronizedTask {
// sychronized(this)代碼塊
public void printA() {
synchronized (this) {
try{
System.out.println("線程" + java.lang.Thread.currentThread().getName() + "開始進入方法printA, 時間:" + System.currentTimeMillis());
java.lang.Thread.currentThread().sleep(3000);
System.out.println("線程" + java.lang.Thread.currentThread().getName() + "開始退出方法printA, 時間:" + System.currentTimeMillis());
}catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
// synchronized方法
synchronized public void printB() {
try{
System.out.println("線程" + java.lang.Thread.currentThread().getName() + "開始進入方法printB, 時間:" + System.currentTimeMillis());
java.lang.Thread.currentThread().sleep(3000);
System.out.println("線程" + java.lang.Thread.currentThread().getName() + "開始退出方法printB, 時間:" + System.currentTimeMillis());
}catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
public class ThreadA extends Thread {
private SynchronizedTask synchronizedTask;
public ThreadA(SynchronizedTask synchronizedTask) {
super();
this.synchronizedTask = synchronizedTask;
}
@Override
public void run() {
synchronizedTask.printA();
}
}
public class ThreadB extends Thread {
private SynchronizedTask synchronizedTask;
public ThreadB(SynchronizedTask synchronizedTask) {
super();
this.synchronizedTask = synchronizedTask;
}
@Override
public void run() {
synchronizedTask.printB();
}
}
public class SynchronizedMain {
public static void main(String[] args) {
SynchronizedTask synchronizedTask = new SynchronizedTask();
ThreadA threadA = new ThreadA(synchronizedTask);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(synchronizedTask);
threadB.setName("B");
threadB.start();
}
}
由于線程A先持有當前對象鎖(synchronizedTask),因此線程A執(zhí)行完printA方法后,線程B才獲得對象鎖犹芹,執(zhí)行printB方法崎页。1.3 sychronized(非this對象)
java支持對“任意對象”作為對象監(jiān)視器來實現同步的功能。鎖非this對象具有一定的優(yōu)點:如果在一個類中有很多個sychronized方法腰埂。這時不同線程調用這些方法雖然能實現同步实昨,但會受到阻塞,影響運行效率盐固。如果使用同步代碼塊鎖非this對象荒给,不同對象監(jiān)視器所在的方法是異步執(zhí)行的,從而提高運行效率刁卜。
public class SynchronizedTask {
private Object object1;
private Object object2;
public SynchronizedTask(Object object1, Object object2) {
this.object1 = object1;
this.object2 = object2;
}
// synchronized(object1)代碼塊
public void printA() {
synchronized (object1) {
try{
System.out.println("線程" + java.lang.Thread.currentThread().getName() + "開始進入方法printA, 時間:" + System.currentTimeMillis());
java.lang.Thread.currentThread().sleep(3000);
System.out.println("線程" + java.lang.Thread.currentThread().getName() + "開始退出方法printA, 時間:" + System.currentTimeMillis());
}catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
// synchronized(object2)代碼塊
public void printB() {
synchronized (object2) {
try{
System.out.println("線程" + java.lang.Thread.currentThread().getName() + "開始進入方法printA, 時間:" + System.currentTimeMillis());
java.lang.Thread.currentThread().sleep(3000);
System.out.println("線程" + java.lang.Thread.currentThread().getName() + "開始退出方法printA, 時間:" + System.currentTimeMillis());
}catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
}
public class ThreadA extends Thread {
private SynchronizedTask synchronizedTask;
public ThreadA(SynchronizedTask synchronizedTask) {
super();
this.synchronizedTask = synchronizedTask;
}
@Override
public void run() {
synchronizedTask.printA();
}
}
public class ThreadB extends Thread {
private SynchronizedTask synchronizedTask;
public ThreadB(SynchronizedTask synchronizedTask) {
super();
this.synchronizedTask = synchronizedTask;
}
@Override
public void run() {
synchronizedTask.printB();
}
}
public class SynchronizedMain {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
SynchronizedTask synchronizedTask = new SynchronizedTask(object1, object2);
ThreadA threadA = new ThreadA(synchronizedTask);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(synchronizedTask);
threadB.setName("B");
threadB.start();
}
}
線程A持有對象鎖object1志电,開始執(zhí)行printA方法后,線程B持有對象鎖object2蛔趴,執(zhí)行printB方法,兩個線程之間互不影響挑辆。1.4 思考
1.4.1 synchronized最終是對對象上鎖
synchronized(class)鎖的是類對象,synchronized(object)鎖的是實例對象
1.4.2 synchronized能同時保證原子性和可見性
在Java內存模型中孝情,synchronized規(guī)定鱼蝉,線程在加鎖時,先清空工作內存→在主內存中拷貝最新變量的副本到工作內存→執(zhí)行完代碼→將更改后的共享變量的值刷新到主內存中→釋放互斥鎖
2. volatile
2.1 保證可見性
volatile可以保證變量在多個線程之間的可見性箫荡,也即每個線程都能夠自動發(fā)現 volatile 變量的最新值魁亦。
2.2 不保證原子性
public class VolatileThread extends Thread {
volatile public static int count;
@Override
public void run() {
addCount();
}
private static void addCount() {
for(int i = 0; i < 100; i++) {
count++;
}
System.out.println("count=" + count);
}
}
public class VolatileMain {
public static void main(String[] args) {
VolatileThread[] threads = new VolatileThread[100];
for(int i = 0; i < 100; i++) {
threads[i] = new VolatileThread();
}
for(int i = 0; i < 100; i++) {
threads[i].start();
}
}
}
下面詳細解釋下上例中volatile出現非線程安全的原因:
(1)read和load階段:從主存復制變量到當前線程工作內存
(2)use和assign階段:執(zhí)行代碼,改變共享變量值
(3)store和write階段:用工作內存數據刷新主存對應變量的值
- volatile的可見性保證羔挡,假如線程1和線程2在進行read和load操作中洁奈,發(fā)現主內存中count的值都是5,那么都會加載這個最新的值绞灼。
- 線程1對count進行加1操作利术,最后將count=6刷新到主內存
- 線程2執(zhí)行時,由于已經進行read和load操作低矮,會在5的基礎上進行加1操作印叁,最后將count=6刷新到主內存
2.3 思考
一般在多線程中使用volatile變量,為了安全军掂,對變量的寫入操作不能依賴當前變量的值:如Num++或者Num=Num5這些操作
3. Atomic
synchronized會導致線程的阻塞轮蜕,從而降低性能。針對i++良姆,i=i+100等操作肠虽,使用synchronized未免太重了幔戏,可通過使用原子類Atomic實現非阻塞同步玛追,它可以在沒有鎖的情況下做到線程安全。
public class AtomicLongTask {
private AtomicLong count = new AtomicLong(0);
public void addCount(){
long startTime = System.currentTimeMillis();
System.out.println(String.format("線程%s執(zhí)行開始時間為%d", Thread.currentThread().getName(), startTime ));
for(int i = 0; i< 1000000000; i++){
count.addAndGet(1);
}
long endTime = System.currentTimeMillis();
System.out.println(String.format("線程%s執(zhí)行結束時間為%d", Thread.currentThread().getName(), endTime ));
System.out.println(String.format("線程%s執(zhí)行時間為%d秒", Thread.currentThread().getName(), (endTime - startTime)/1000 ));
System.out.println(String.format("線程%s, count: %d秒", Thread.currentThread().getName(), count.get() ));
}
public AtomicLong getCount() {
return count;
}
public void setCount(AtomicLong count) {
this.count = count;
}
}
public class AtomicLongThread extends Thread{
AtomicLongTask atomicLongTask;
public AtomicLongThread(AtomicLongTask atomicLongTask) {
this.atomicLongTask = atomicLongTask;
}
@Override
public void run() {
atomicLongTask.addCount();
}
}
public class AtomicLongMain {
// A線程和B線程分別執(zhí)行1000000000次++操作, 總耗時為42s左右,使用AtomicLong,性能明顯優(yōu)于Synchronized
public static void main(String[] args){
AtomicLongTask atomicLongTask = new AtomicLongTask();
AtomicLongThread t1 = new AtomicLongThread(atomicLongTask);
t1.setName("A");
AtomicLongThread t2 = new AtomicLongThread(atomicLongTask);
t2.setName("B");
t1.start();
t2.start();
}
}
A線程和B線程分別執(zhí)行1000000000次++操作, 總耗時為42s左右,使用AtomicLong,性能明顯優(yōu)于Synchronized(讀者可自行試驗)