当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果。那么这个对象就是线程安全的。
Java 语言中得各种操作共享数据可以分成五类,按安全程度由强到弱来排序:
1 不可变:不可变的对象一定是线程安全的,无论对象的方法实现还是方法的调用都不需要在采取措施。
如果共享数据是一个基本数据类型,那么只要在定义的时候使用final 关键字修饰就可以保证它不可变;如果共享数据是一个对象,需要对象自行保证其行为不会对其状态产生任何影响。
2 绝对线程安全:绝对线程安全是不管运行环境如何,调用者都不需要任何额外的同步措施。通常需要付出很大的甚至不切实际的代价。
3 相对线程安全:就是通常意义的线程安全,确保这个对象单独的操作是安全的。Java语言中,大部分声称线程安全的类都属于这种类型,如Vector,Hashtable。
4 线程兼容:指的是本身对象并不是线程安全的,但是可以通过调用端的正确使用同步手段来保证对象的安全使用。通常我们说一个类不是线程安全的,指的就是这种状态,如:ArrayList,HashMap等。
5 线程对立:无论调用端是否采取了同步措施,都无法在多线程环境中并发使用。
互斥和同步是一种最常见、最主要的实现并发正确性(相对线程安全)的保障手段。
同步:指的是多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一个线程使用。
互斥:指的是实现同步的一种手段,临界区互斥量和信号量都是主要的互斥实现方式。
在java中,最基本的互斥同步手段就是synchronized关键字。synchronized可以保证线程竞争共享资源的正确性(多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一个线程使用)。
原子性:确保线程互斥的访问同步代码。synchronized保证只有一个线程拿到锁,进入同步代码块操作共享资源,因此具有原子性。
可见性:保证共享变量的修改能够及时课件。执行synchronized时,会对应执行lock,unlock原子操作。lock操作,就会清空工作空间该变量的值;执行unlock操作之前,必须先把变量同步回主内存中。
有序性:synchronized内的代码和外部的代码禁止排序,至于内部的代码,则不会禁止排序,但是由于只有一个线程进入同步代码块,因此在同步代码块中相当于是单线程的,根据as-if-serial语义,即使代码块内部发生了重排序,也不会影响程序执行的结果。
悲观锁:synchronized是悲观锁,每次使用共享资源时都认为会和其他线程产生竞争,所以每次使用共享资源都会上锁。
独占锁:synchronized是独占锁,该锁一次只能被一个线程所持有,其他线程被阻塞。
非公平锁:synchronized是非公平锁,线程获取锁的顺序可以不按照线程的阻塞顺序,允许线程发出请求后立即尝试获取锁。
可重入锁:synchronized是可重入锁。持锁线程可以再次获取自己的内部的锁。
Tips:
1 悲观锁 or 乐观锁:是否一定要锁。
2 共享锁 or 独占锁(排他锁):是否可以有多个线程同时拿锁。
3 公平锁 or 非公平锁:是否按阻塞顺序拿锁。
4 可重入锁 or 不可重入锁: 拿锁线程是否可以多次拿锁。
作用于给当前实例加锁,进入同步代码前要获得当前实例的锁。(注意:是当前实例,一个类有N个实例,一个实例有一把锁)。
使用synchronized 修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着,且共享对象一定是this,并且同步代码块是整个方法体。
示例1
public class HasSelfPrivateNum {
private int num = 0;
synchronized public void addI(String username){
try {
if ("a".equals(username)){
num = 100;
System.out.println("a set over! " + System.currentTimeMillis());
Thread.sleep(2000);
}else {
num = 200;
System.out.println("b set over! " + System.currentTimeMillis());
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.addI("a");
}
}
public class ThreadB extends Thread{
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.addI("b");
}
}
public class TestRun {
public static void main(String[] args) {
HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();
ThreadA athread = new ThreadA(numRef1);
athread.start();
ThreadB bthread = new ThreadB(numRef2);
bthread.start();
}
}
synchronized numRef1 和 numRef2 是两个不同的类,同步方法的锁对象不是同一个对象,无法达到同步的目的,解决方法是让synchronized 锁对象是同一个,示例代码如下,运行结果如下图。
public class TestRun {
public static void main(String[] args) {
HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
//HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();
ThreadA athread = new ThreadA(numRef1);
athread.start();
ThreadB bthread = new ThreadB(numRef1);
bthread.start();
}
}
运行结果如图:
表示找类锁,类锁永远只有一把,就算创建了100个对象,那类锁也只有一把。
对象锁:一个对象一把锁,
类锁:100个对象,也是一把锁。
当synchronized 作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static sync