什么是线程同步
多线程共享数据时,会发生线程不安全的情况,多线程共享数据必须同步。 线程同步其实就是一种等待机制,多个线程需要同时访问同一个对象,则线程进入这个对象的等待池(wait pool)形成队列,等待前面的线程使用完毕后,下一个线程再使用。
来看以下例子:
public class ThreadSafeTest implements Runnable{
int num=100;
@Override
public void run(){
while(true){
if (num>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("tickets"+--num);
}else break;
}
}
public static void main(String args[]){
ThreadSafeTest t= new ThreadSafeTest();
Thread threadA=new Thread(t);
Thread threadB=new Thread(t);
Thread threadC=new Thread(t);
Thread threadD=new Thread(t);
threadA.start();
threadB.start();
threadC.start();
threadD.start();
}
}
这段代码的意思是建立四个线程共同对num的值进行修改,当num大于零时停止输出。
执行情况
可以看到结果并不是顺序输出而且还出现的为负的情况。这就是因为在进入if()之后其他进程对num进行了修改。
这时就需要用到线程同步
Monitor对象
什么是Monitor?
Monitor是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象,主要特点是:
对象的所有方法都被“互斥”的执行。好比一个Monitor只有一个运行“许可”,任一个线程进入任何一个方法都需要获得这个“许可”,离开时把许可归还。
Java对象是天生的Monitor。每一个Java对象都有成为Monitor的“潜质”。这是为什么?因为在Java的设计中,每一个对象出来后,就带了一把看不见的锁,通常我们叫“内部锁”,或者“Monitor锁”,或者“Intrinsic lock”。有了这个锁的帮助,只要把类的所有对象方法都用synchronized关键字修饰,并且所有域都为私有(也就是只能通过方法访问对象状态),就是一个货真价实的Monitor了。
java线程同步的方法
一、synchronized
使用 synchronized关键字,可以修饰普通方法、静态方法,以及语句块。上面说了java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。需要注意的是调用静态方法时,锁住的不是对象,锁住的是类。
//修饰普通方法
public synchronized void test(){}
//修饰语句块
synchronized(Object){
}
//修饰静态方法
public static synchronized int tests(){
}
在需要同步的地方用synchronized修饰
public void run(){
while(true){
synchronized (""){
if (num>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("tickets"+--num);
}else break;
}
}
}
此时达到预期结果
二、wait和notify
由于线程时循环判断的,所以线程会不断判断此时synchronized (lock)中的lock是否能获得,这样会造成一些浪费。所以要使用wait()和notify()方法。
wait()和notify()方法,是属于对象的,不是属于线程的。它们用在线程同步时,synchronized语句块中。在synchronized语句块中,同一个对象,一个线程在执行完这一块代码之前,另一个线程,如果传进来的是同一个object,是不能进入这个语句块的。wait()意思是,当前线程进入等待状态直到被唤醒。notify()意思是叫醒一个等待的线程。
也就是说,wait()会让出对象锁,同时,当前线程休眠,等待被唤醒,如果不被唤醒,就一直等在那儿。notify()会唤醒休眠的线程。
三、volatile
使用特殊域变量volatile实现线程同步。volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性。可见性的意思是当一个线程修改一个共享变量时另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
volatile关键字为域变量的访问提供了一种免锁机制, 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新, 因此每次使用该域就要重新计算,而不是使用寄存器中的值。
class VolatileFeaturesExample {
volatile long vl = 0L; // 使用volatile声明64位的long型变量
public void set(long l) {
vl = l; // 单个volatile变量的写
}
public void getAndIncrement () {
vl++; // 复合(多个)volatile变量的读/写
}
public long get() {
return vl; // 单个volatile变量的读
}
}
等价于
class VolatileFeaturesExample {
long vl = 0L; // 64位的long型普通变量
public synchronized void set(long l) { // 对单个的普通变量的写用同一个锁同步
vl = l;
}
public void getAndIncrement () { // 普通方法调用
long temp = get(); // 调用已同步的读方法
temp += 1L; // 普通写操作
set(temp); // 调用已同步的写方法
}
public synchronized long get() { // 对单个的普通变量的读用同一个锁同步
return vl;
}
}
四、ReentrantLock
ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。
4.1ReentrantLock和synchronized的区别
-
Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
-
Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
-
ReentrantLock和synchronized都是可重入的。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。
-
ReenreantLock可以实现公平锁。公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁则随机分配这种使用权。和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。在创建ReentrantLock的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁
ReentrantLock lock = new ReentrantLock(true);
4.2ReentrantLock可响应中断
当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()。该方法可以用来解决死锁问题。
4.3获取锁时限时等待
ReentrantLock还给我们提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
ReenreantLock类的常用方法有:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
}
关于Lock对象和synchronized关键字的选择:
a.最好两个都不用,使用一种java.util.concurrent包提供的机制, 能够帮助用户处理所有与锁相关的代码。
b.如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码
c.如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁