Java 线程同步
综述
我们知道,当不同的线程对同一个变量进行操作的时候,将会有可能出现数据不统一的情况。
为了解决这种问题,Java提供了几种方式:synchronized关键字,Lock锁机制,ThreadLocal线程副本的方式。
synchronized关键字
synchronized是一种互斥锁,如果对临界资源加上互斥锁,当一个线程在访问该临界资源时,其他线程便只能等待。
在Java中每一个对象都拥有一个锁标记(monitor),也称为监视器,多线程同时访问某个对象时,只有获取了该对象的锁的线程才能访问。
synchronized关键字主要有如下的两种使用方式
同步代码块
通过同步代码块对多线程环境下的共享的变量进行限制,同一时间只允许一个线程对共享的变量进行修改,知道该线程释放这个锁为止。
Integer i=0;
void set(int count){
synchronized (i){ //对i进行加锁,同一时间仅仅允许一个线程对i进行修改
i+=count;
}
}
同步方法
即通过synchronized修饰的方法,当我们通过synchronized方法修饰的时候,将对整个方法加锁,在没有释放这个锁之前,其他的线程无法对该方法进行操作。
值得注意的是因为一个对象只有一个锁,所以当一个线程获得了一个对象的锁后,其他的线程也不能访问该对象的其他的synchronized方法,但是可以访问其中的非synchronized方法。
如下是一个简单的synchronized方法的例子
int i=0;
synchronized void set(int count){ //对方法进行synchronized修饰
i+=count;
}
Lock锁机制
Lock是Java5后面出现的一种新的同步访问的方法。
相较于synchronized,Lock提供了一种更加灵活的加锁机制。
当我们使用synchronized后,如果一个由于等待Io或者调用sleep等方法的问题线程被阻塞,同时他将不会释放锁,其他的线程就会一直等待。
同时如果我们通过同步代码块对一个变量进行操作时候,这个时候读操作和读操作应该不会互斥的,但是由于synchronized机制的问题,我们必须等待一个线程对该变量操作完成后,才能进行另外一个线程的操作。
这里主要介绍一下重入锁和读写锁
ReentrantLock重入锁
ReentrantLock实现了lock接口,重入锁是基于线程分配,而不是基于方法分配,例如下面的例子
Integer i=0;
synchronized void set(int count){
get(); //由于重入锁机制,不在需要添加synchronized
i+=count;
}
void get(){
}
在代码中通过ReentrantLock实现Lock加锁
private Lock lock = new ReentrantLock();
void set(){
lock.lock(); //加锁
try {
for(;;) {
//进行一些操作
}
} finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock(); //释放锁
}
}
值得注意的是在这里private Lock lock = new ReentrantLock(); 需要声明为类变量,如果在set函数中加上如上的代码,不会起到同步的作用,因为每一个线程执行到这里都会得到一个新的Lock实例。
tryLock()方法
tryLock方法体现了Lock方法的灵活性,tryLock会尝试获得一个锁,不论获取成功与否都会直接返回,也就不会一直被阻塞。
lockInterruptibly()方法。
在锁上等待,直到获取锁,但是会响应中断,这个方法优先考虑响应中断,而不是响应锁的普通获取或重入获取。其实也就是一个特别的锁,我们知道当线程被阻塞时候如果对其进行中断操作是没有反应的,因为这个没有sleep,wait等条件让其产生中断异常,这个时候如果当线程被阻塞的时候,我们就可以使用lock.lockInterruptibly()方法,当其被阻塞时候,如果调用其Interrupt方法,将会优先相应中断异常。
如下代码所示
void set() throws InterruptedException {
lock.lockInterruptibly(); //通过lock.lockInterruptibly(); 进行锁的调用
try {
for (int i=0;i<10;i++){
Log.e("123",Thread.currentThread().getName()+i); //循环打印10次
try {
Thread.sleep(1000); //休眠1秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
finally {
lock.unlock(); //最后释放锁
}
}
class Th implements Runnable{ //测试线程类
@Override
public void run() {
try {
set(); //调用set会抛出中断异常
} catch (InterruptedException e) {
e.printStackTrace();
Log.e("123",Thread.currentThread().getName()+"我被中断了"); //如果中断了打印中断
}
Log.e("123",Thread.currentThread().getName()+"我做完了"); //完成了操作
}
}
执行代码
Thread th=new Thread(new Th(), "one");
Thread th1=new Thread(new Th(),"two");
th.start();
th1.start();
th1.interrupt(); //对th1进行中断操作。
执行结果如下
one0
two我被中断了
two我做完了 //优先相应中断 ,直接捕获中断异常
one1 //接着继续执行one的操作
one2
one3
one4
one5
one6
one7
one8
one9
one我做完了
ThreadLocal
ThreadLocal,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
关于ThreadLocal我在Android 的handler机制中对其进行了解读,因为他可以让每一个线程拥有自己的副本变量,所以可以很轻松的实现不同线程的并发问题。