Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程调用,从而保证了该变量的唯一性和准确性。
Java提供了一种内置的锁机制来支持原子性。每个Java对象都可以用作一个实现同步的锁,称为内置锁。线程进入同步代码块之前自动获取到锁,代码块执行完成正常退出或代码块中抛出异常退出时会释放掉锁。
内置锁为互斥锁,即线程A获取到锁后,线程B阻塞直到线程A释放锁,线程B才能获取到同一个锁;内置锁使用synchronized关键字实现。
即有synchronized关键字修饰的方法(所有访问状态变量的方法都必须进行同步),此时充当锁的对象为调用同步方法的对象。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。如下代码:
public class Test{
private int count = 0;
public synchronized int add(){
count += 1;
return count;
}
public synchronized int delete(){
count -= 1;
return count;
}
}
synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
即有synchronized关键字修饰的语句块。锁的粒度更细,并且充当锁的对象不一定是this,也可以是其它对象,使用起来更加灵活。
public class Test{
private int count = 0;
public int add(){
synchronized(this){
count += 1;
}
return count;
}
public int delete(){
synchronized(this){
count -= 1;
}
return count;
}
}
同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。下面看几个例子:
public class Status {
private int num = 0;
public void selfIncrease(){
num = num + 1;
System.out.println(Thread.currentThread().getName()
+ "|" + num);
}
}
public class Task implements Runnable {
private Status status;
public Task(Status status){
this.status = status;
}
public void run() {
synchronized (status) {
status.selfIncrease();
}
}
public static void main(String[] args) {
Status status = new Status();
Task task = new Task(status);
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
Thread t3 = new Thread(task);
Thread t4 = new Thread(task);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
从代码中可以看出,四个线程使用同一个Status对象。运行结果如下:
Thread-0|1
Thread-2|2
Thread-1|3
Thread-3|4
修改一下上面的代码,每个线程运行时创建一个新的Status对象,如下:
public class Task implements Runnable {
public void run() {
Status status = new Status();
synchronized (status) {
status.selfIncrease();
}
}
public static void main(String[] args) {
Task task = new Task();
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
Thread t3 = new Thread(task);
Thread t4 = new Thread(task);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
由于充当锁的对象实例不一定是同一个对象(hashcode不同),同步失效。 运行结果如下:
Thread-2|1
Thread-3|1
Thread-1|1
Thread-0|1
由上面两段代码,可以看出
同步代码块中充当锁的对象必须为同一个对象。
我们再看一段代码,如下:
public class Task implements Runnable {
private Status status;
public Task(Status status){
this.status = status;
}
public void run() {
synchronized (status) {
System.out.println("Thread lock");
System.out.println("Thread:" + status.getNum());
System.out.println("Thread over");
}
}
public static void main(String[] args) {
Status status = new Status();
Task task = new Task(status);
Thread t = new Thread(task);
t.start();
//synchronized(status){
System.out.println("Main");
status.setNum(1);
System.out.println("Main:" + status.getNum());
//}
}
}
执行结果如下:
Main
Thread lock
Main:1
Thread:1
Thread over
从运行结果可以看出,在Thread线程锁定Status对象的时候,Main线程在Thread线程释放锁对象前依然能够修改Status对象的num域,说明锁没有生效。想一下这是为什么呢?
在开始部分我们讲到内置锁也就是互斥锁的介绍时,线程A获得互斥锁后,线程B阻塞知道线程A释放互斥锁,线程B才能获取到同一个互斥锁。但是上面代码中Main线程并没有对Status对象进行同步,故在Thread线程锁定Status对象的时候不需要阻塞,可以直接操作Status对象,因此,所有使用同步对象的地方都必须进行同步。
修改方式是把注释掉的代码打开。
如果锁对象为静态变量,或使用synchronized关键字修饰静态方法,则锁对象为Class对象。
public class Status {
private static int num = 0;
public synchronized static void selfIncrease(){
num = num + 1;
System.out.println(Thread.currentThread().getName()
+ "|" + num);
}
}
public class Task implements Runnable {
public void run() {
Status.selfIncrease();
}
public static void main(String[] args) throws Exception {
Task task = new Task();
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
Thread t3 = new Thread(task);
Thread t4 = new Thread(task);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果如下:
Thread-0|1
Thread-2|2
Thread-1|3
Thread-3|4
相当于:
public class Status {
private static int num = 0;
public static void selfIncrease(){
synchronized(Status.class){
num = num + 1;
System.out.println(Thread.currentThread().getName()
+ "|" + num);
}
}
}
或者:
public class Status {
private static int num = 0;
private static Object lock = new Object();
public static void selfIncrease(){
synchronized(lock){
num = num + 1;
System.out.println(Thread.currentThread().getName()
+ "|" + num);
}
}
}
(1).volatile关键字为域变量的访问提供了一种免锁机制;
(2).使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新;
(3).因此每次使用该域就要重新计算,而不是使用寄存器中的值;
(4).volatile不会提供任何原子操作,它也不能用来修饰final类型的变量;
示例如下:
class Bank {
//需要同步的变量加上volatile
private volatile int account = 100;
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
account += money;
}
}
多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。
在Java1.5中新增了以个java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它拥有synchronized相同的并发性和内存语义,此外还多了锁投票,定时锁等候和中断锁等候。
线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定;
如果使用synchronized,如果A不释放,B将一直等下去,不能被中断;
如果使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情。
ReentrantLock类常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
class Bank {
private int account = 100;
//需要声明这个锁
private Lock lock = new ReentrantLock();
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
lock.lock();
try{
account += money;
}finally{
lock.unlock();
}
}
}
注意:Lock对象和synchronized关键字除了上面讲到的区别之外,还有下面不同的地方:
(1).synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行出现异常时,JVM会自动释放锁定;但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中。
(2).在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock;但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal类的常用方法:
ThreadLocal() : 创建一个线程本地变量
get() : 返回此线程局部变量的当前线程副本中的值
initialValue() : 返回此线程局部变量的当前线程的"初始值"
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
上面的例子可改写为:
public class Bank{
//使用ThreadLocal类管理共享变量account
private static ThreadLocal account = new ThreadLocal(){
@Override
protected Integer initialValue(){
return 100;
}
};
public void save(int money){
account.set(account.get()+money);
}
public int getAccount(){
return account.get();
}
}
注意:ThreadLocal与同步机制
(1).都是为了解决多线程中相同变量的访问冲突问题。
(2).ThreadLocal采用以“空间换时间”的方法,同步机制采用以“时间换空间”的方式。