一. 对象及变量的并发访问
非线程安全会发生在多个线程并发访问同一个对象的实例变量时,会产生脏读,读取到的数据是被更改过的。而线程安全各个线程获得的实例变量的值都是经过同步处理的,不会出现脏读。
1.线程是否安全呢?
(1) 如果是方法内部的私有变量,不存在非线程安全问题。
(2) 多线程访问的是同一个对象的实例变量时,有可能出现线程不安全问题。
(3) 多个线程访问的是同步方法的话,一定是线程安全的。
(4) 多个线程对应的是多个对象时,出现的结果就会是异步的,但是线程安全。
2. synchronized关键字
(1) synchronized获得的是对象锁,而不是把synchronized下面的方法或者代码块当做锁。
(2) synchronized声明的方法一定是排队执行的(同步的),只有共享资源的读写访问才需要同步化。
(3) 线程A和线程B访问同一个object对象的两个同步的方法,线程A先获取object对象的Lock锁,B线程可以以异步的方式调用object对象中的非同步方法,但是想访问该对象的同步方法的话,必须得等待,不管想访问的是不是和线程A同一个同步方法。
(4) synchronized有锁重入的功能,即自己可以再次获取自己的内部锁,可重入锁也支持父子类继承关系中,即子类可以通过可重入锁访问父类的方法。若不可重入的话,就会造成死锁。
(5) 当一个线程执行的代码出现异常时,其持有的锁会自动释放(即该线程结束执行)。
(6) 同步不具有继承性。
(7) 锁定的对象改变,比如String,可能导致同步锁无效(因为锁变了)。但是只要对象不变,对象的属性被改变,锁还是同一个。
3.synchronized同步语句块
synchronized同步方法是对当前对象加锁,同步代码块则是对某一个对象加锁。synchronized同步代码块运行效率应该大于同步方法。
synchronized(this):也是锁定当前对象的。
synchronized(非this对象):使用同步代码块来锁定非this对象,则synchronized(非this对象)与同步方法是异步的,不与其他锁this同步方法争抢this锁,可以大大提高效率。synchronized同步代码块都不采用String作为锁对象,易造成死锁。
4.synchronized关键字加到static静态方法上是给Class类加上锁(Class锁可以对类得所有对象实例起作用),而加到非static静态方法上是给对象上锁。
synchronized关键字加到static静态方法上是给Class类加上锁 = synchronized(xxx.Class){}
5.多线程的死锁
因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续执行。
比如线程A持有了锁1在等待锁2,线程A持有了锁2在等待锁1--》导致死锁。
解决方案:不使用嵌套的synchronized代码结构。
6.内置类与静态内置类(补充介绍)
非静态内置类:指定对象.new 内置类();
静态内置类:可直接new 内置类();
7.volatile关键字:使变量在多个线程中可见
作用:强制从公共堆栈中获取变量的值,而不是从线程私有数据栈中获取。
※ 在JVM被设置为-server模式时是为了线程运行的效率,线程一直在私有堆栈中获取变量的值。
在-server模式下,公共堆栈的值和线程私有数据栈的值不同步,加了volatile后就会强制从公共堆栈中读写。
※volatile和synchronized的比较:
(1) volatile只能修饰变量,是轻量级实现,所以性能比synchronized好。
(2) 多线程访问volatile不会阻塞,而访问synchronized会阻塞。
(3) volatile能保证数据可见性,但不具备同步性,不支持原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为他会将私有内存和共有内存中的数据做同步。
(4) 两者功能属性不同,synchronized解决的是多个线程之间访问资源的同步性;
volatile解决变量在多个线程之间的可见性,即:在多个线程可以感知实例变量被修改了,并且可以获得最新的值引用,也就是用多线程读取共享变量时能获得最新值引用。
volatile int i
i++;
i++有如下三个步骤:
(1) 从内存中获取i的值;
(2)计算i的值;
(3) 将i的值写入内存中。
这样的操作不是一个原子操作(联想:synchronized修饰的方法或者代码段可以看做一个整体,因此具有原子性),比如线程B要提取i的值时,线程A还未将计算好的i的值放回内存,则线程B取出来的i的值还是线程A计算前的值。--》线程不安全
8.AtomicInteger(AtomicLong等)
private AtomicInteger count = new AtomicInteger(0);
System.out.println(count.incrementAndGet());//自动加1//decrementAndGet()自动减1
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
Compare And Swap(CAS):首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。最后,CPU 会将旧的数值返回。这一系列的操作是原子的。简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。下面来看一下AtomicInteger是如何利用CAS实现原子性操作的。
但是CAS也是有问题存在的:
CAS的ABA问题
1.进程P1在共享变量中读到值为A
2.P1被抢占了,进程P2执行
3.P2把共享变量里的值从A改成了B,再改回到A,此时被P1抢占。
4.P1回来看到共享变量里的值没有被改变,于是继续执行。
虽然P1以为变量值没有改变,继续执行了,但是这个会引发一些潜在的问题。ABA问题最容易发生在lock free 的算法中的,CAS首当其冲,因为CAS判断的是指针的地址。如果这个地址被重用了呢,问题就很大了。(地址被重用是很经常发生的,一个内存分配后释放了,再分配,很有可能还是原来的地址)
还有一种情况是:单独一个AtomicInteger.incrementAndGet()是线程安全的,但是同时两个AtomicInteger.incrementAndGet()就不一定是线程安全的了,即两个方法之间不是原子的。
public static AtomicLong count = new AtomicLong();
public void addNum(){
System.out.println(count.addAndGet(100));
System.out.println(count.addAndGet(1));
}
多个线程调用addNum()时,线程A加了100,还没来得及加1,线程B就进来加了100。
解决方案:
public static AtomicLong count = new AtomicLong();
synchronized public void addNum(){
System.out.println(count.addAndGet(100));
System.out.println(count.addAndGet(1));
}
9.synchronized代码块也具有volatile同步的功能
线程A调用runMethod(),线程B调用stopMethod(),持有的是同一把锁。
当线程A调用完runMethod()后,打印不出"停下来了!"的,因为死循环,被A锁死了。
各线程间的数据值没有可见性。
private Boolean isContinueRun = true;
public void runMethod(){
while(isContinueRun){
}
System.out.println("停下来了!");
}
public void stopMethod(){
isContinueRun = false;
}
解决方案如下,成功打印"停下来了!"
private Boolean isContinueRun = true;
public void runMethod(){
private anyString = new String();
while(isContinueRun){
synchronized(anyString){
}
}
System.out.println("停下来了!");
}
public void stopMethod(){
isContinueRun = false;
}
关键字synchronized 保证同一时刻,只有一个线程可以执行某一个方法或某一个代码块。包含两种特性:互斥性和可见性。
不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或代码块的每个线程,都可以看到由同一个锁保护之前所有的修改结果。(外连互斥,内修可见。)
二. 锁的使用
Lock也能实现同步的效果,在使用上更加方便。
1. ReentrantLock类 -- Lock lock = new ReentrantLock();
(1) 使用ReentrantLock.lock()获取锁(加锁),线程就拥有了“对象监视器”;
其他线程只有等待ReentrantLock.unlock()释放锁(解锁),再次争抢获得锁。
效果和synchronized一致,但线程执行顺序是随机的。
(2) 关键字与wait()/notify()/notifyAll():实现等待/通知模式,但是notifyAll()的话,需要通知所有处于WAITING状态的线程,会出现相当大的效率问题。
ReentrantLock和Condition对象也同样可以实现。在一个Lock对象里可以创建多个Condition(即对象监视器)实例,可以实现多路通知功能;实例对象可以注册在指定的Condition中,从而可以有选择地进行线程通知,在调度线程上更加灵活。
package lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class LockService {
private Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
public void await(){
try {
lock.lock();
condition.await();//必须在调用await()之前先调用lock()以获得同步监视器
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void signal(){
try {
lock.lock();
condition.signal();//还有condition.signalAll();
} finally{
lock.unlock();
}
}
}
(3) signalAll()
public Condition conditionA = lock.newCondition();
public Condition conditionB = lock.newCondition();
//use:
conditionA.signal(); //or conditionB.signal();
唤醒指定种类的线程,如conditionA.signal(); 只有用了conditionA的线程被唤醒。
(4) 公平锁与非公平锁
公平锁:线程获取锁的顺序是按照线程加锁的顺序来分配的(FIFO);new ReentrantLock(true);//不一定百分百FIFO,但是基本呈有序。
非公平锁(默认):锁的抢占机制,随机获得锁。new ReentrantLock(false);
2.相关方法介绍
(1) int getHoldCount():查询当前线程保持此锁定的个数,也就是调用lock()的次数。
(2) int getQueueLength():返回正获取此锁定的线程估计数,如5个线程,一个线程调用了await(),还有4个线程在等待锁的释放。
(3) int getWaitQueueLength(Condition condition):比如有5个线程,每个线程都执行了同一个condition对象的await(),则结果为5。
(4) boolean hasQueuedThread(Thread thread):查询指定的线程是否正在等待此锁定。
(5) boolean hasWaiters(Condition condition):是否有线程正在等待与此锁定有关的condition条件。
(6) boolean isFair():判断是不是公平锁。
(7) boolean isHeldByCurrentThread():查询当前线程是否保持此锁定。
(8) boolean isLocked():查询此锁定是否由任意线程保持。
(9) void lockInterruptibly():如果当前线程未被中断,则获取锁定,如果已经被中断则出现异常。
(10) boolean tryLock():仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定。
(11) boolean tryLock(long timeout,TimeUnit unit):若在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。
(12) Condition.awaitUninterruptibly():在WAITING情况下interrupt()不会抛出异常。
(13) Condition.awaitUntil(time):线程在等待时间到达前,可以被其他线程唤醒。
3.ReentranReadWriteLock类
共享锁:读操作相关的锁;排他锁:写操作相关的锁。
读写,写读,写写都是互斥的;读读是异步的,非互斥的。
网友评论