线程安全需要保证几个基本特性:
- 原子性:相关操作不会中途被其他线程干扰,一般通过同步实现
- 可见性:一个线程修改了某个共享变量,其状态能够立即被其它线程知晓
- 有序性:保证线程内串行语义,避免指令重排等
1. synchronized
1.1 sychronized(class)代码块与静态同步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());
}
}
}
// 静态同步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执行完printA方法后,线程B才获得class锁,执行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执行完printA方法后,线程B才获得对象锁,执行printB方法。1.3 sychronized(非this对象)
java支持对“任意对象”作为对象监视器来实现同步的功能。锁非this对象具有一定的优点:如果在一个类中有很多个sychronized方法。这时不同线程调用这些方法虽然能实现同步,但会受到阻塞,影响运行效率。如果使用同步代码块锁非this对象,不同对象监视器所在的方法是异步执行的,从而提高运行效率。
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,开始执行printA方法后,线程B持有对象锁object2,执行printB方法,两个线程之间互不影响。1.4 思考
1.4.1 synchronized最终是对对象上锁
synchronized(class)锁的是类对象,synchronized(object)锁的是实例对象
1.4.2 synchronized能同时保证原子性和可见性
在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁
2. volatile
2.1 保证可见性
volatile可以保证变量在多个线程之间的可见性,也即每个线程都能够自动发现 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阶段:执行代码,改变共享变量值
(3)store和write阶段:用工作内存数据刷新主存对应变量的值
- volatile的可见性保证,假如线程1和线程2在进行read和load操作中,发现主内存中count的值都是5,那么都会加载这个最新的值。
- 线程1对count进行加1操作,最后将count=6刷新到主内存
- 线程2执行时,由于已经进行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执行开始时间为%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执行结束时间为%d", Thread.currentThread().getName(), endTime ));
System.out.println(String.format("线程%s执行时间为%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线程分别执行1000000000次++操作, 总耗时为42s左右,使用AtomicLong,性能明显优于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线程分别执行1000000000次++操作, 总耗时为42s左右,使用AtomicLong,性能明显优于Synchronized(读者可自行试验)