java线程安全(五)
1.线程安全
当多个线程访问一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。
2.线程同步
1、线程同步的目的是为了保护多个线程访问一个资源时对资源的破坏。
2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法。
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、对于同步,要时刻清醒在哪个对象上同步,这是关键。
5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。
2.1 synchronized关键字
在Java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。 如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。
根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。 如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。 如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。在虚拟机规范对monitorenter和monitorexit的行为描述中,有两点是需要特别注意的。 首先,synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。 其次,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。对于代码简单的同步块(如被synchronized修饰的getter()或setter()方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。 所以synchronized是Java语言中一个重量级(Heavyweight)的操作,有经验的程序员都会在确实必要的情况下才使用这种操作。 而虚拟机本身也会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态之中。
synchronized的具体用法
synchronized关键字的作用域有二种:
1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法。
2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是:
synchronized(this){
/*区块*/
}
它的作用域是当前对象;不写默认当前对象。
3)synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法synchronized方法;总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。每个对象只有一个锁(lock)与之相关联。
3.ReetrantLock(1.5)
除了synchronized之外,我们还可以使用java.util.concurrent(下文称J.U.C)包中的重入锁ReentrantLock来实现同步,在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性,只是代码写法上有点区别,一个表现为API层面的互斥锁(lock()和unlock()方法配合try/finally语句块来完成),另一个表现为原生语法层面的互斥锁。不过,相synchronized,ReentrantLock增加了一些高级功能,主要有以下3项:等待可中断、 可实现公平锁,以及锁可以绑定多个条件。reentrant 锁有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了synchronized的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。
private ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
private ReentrantLock lock = new ReentrantLock(true); //公平锁
不公平锁与公平锁的区别:
公平情况下,操作会排一个队按顺序执行,来保证执行顺序。(会消耗更多的时间来排队)不公平情况下,是无序状态允许插队,jvm会自动计算如何处理更快速来调度插队。(如果不关心顺序,这个速度会更快)每一个Lock可以有任意数据的Condition对象,Condition是与Lock绑定的,所以就有Lock的公平性特性:如果是公平锁,线程为按照FIFO的顺序从Condition.await中释放,如果是非公平锁,那么后续的锁竞争就不保证FIFO顺序了lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放。
Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。在Condition中,await()对应于Object.wait(),signal()对应于Object.notify(),signalAll()对应于Object.notifyAll()。传统线程的通信方式,Condition都可以实现,这里注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。
这样看来,Condition和传统的线程通信没什么区别,Condition的强大之处在于它可以为多个线程间建立不同的Condition.特别说明的是Condition的接口改变名称就是为了避免与Object中的wait/notify/notifyAll的语义和使用上混淆,因为Condition同样有wait/notify/notifyAll方法。
Lock(),unLock实现同步
public class MyThread extends Thread {
private MyService myService;
MyThread(MyService myService) {
super();
this.myService = myService;
}
@Override
public void run() {
myService.add();
}
public static void main(String[] args) {
MyService myService = new MyService();
MyThread myThread1 = new MyThread(myService);
MyThread myThread2 = new MyThread(myService);
myThread1.start();
myThread2.start();
}
}
public class MyService {
private Lock lock = new ReentrantLock();
public void add() {
lock.lock();
for (int i = 0; i < 5; i++) {
System.out.println("-----" + Thread.currentThread().getName() + "-----i=" + i);
}
lock.unlock();
}
}
-----Thread-0-----i=0
-----Thread-0-----i=1
-----Thread-0-----i=2
-----Thread-0-----i=3
-----Thread-0-----i=4
-----Thread-1-----i=0
-----Thread-1-----i=1
-----Thread-1-----i=2
-----Thread-1-----i=3
-----Thread-1-----i=4
Condition实现部分通知,特定通知
public class MyThread extends Thread {
private MyService myService;
MyThread(MyService myService) {
super();
this.myService = myService;
}
@Override
public void run() {
myService.await();
}
public static void main(String[] args) {
MyService myService = new MyService();
MyThread myThread1 = new MyThread(myService);
myThread1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
myService.signal();
}
}
public class MyService {
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
public void await() {
try {
lock.lock();
System.out.println("等待开始时间为:" + System.currentTimeMillis());
condition1.await();
System.out.println("等待结束时间为:" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signal(){
try {
lock.lock();
System.out.println("唤醒时间为:" + System.currentTimeMillis());
condition1.signal();
} finally {
lock.unlock();
}
}
}
等待开始时间为:1516675014227
唤醒时间为:1516675014721
await(),Singnal(),signalAll()通知
public class MyThread extends Thread {
private MyService myService;
MyThread(MyService myService) {
super();
this.myService = myService;
}
@Override
public void run() {
myService.await1();
myService.await2();
}
public static void main(String[] args) {
MyService myService = new MyService();
MyThread myThread1 = new MyThread(myService);
myThread1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
myService.signal1();
myService.signal2();
}
}
public class MyService {
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
public void await1() {
try {
lock.lock();
System.out.println("等待开始时间为:" + System.currentTimeMillis());
condition1.await();
System.out.println("等待结束时间为:" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signal1(){
try {
lock.lock();
System.out.println("唤醒时间为:" + System.currentTimeMillis());
condition1.signalAll();
} finally {
lock.unlock();
}
}
public void await2() {
try {
lock.lock();
System.out.println("等待开始时间为:" + System.currentTimeMillis());
condition2.await();
System.out.println("等待结束时间为:" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signal2(){
try {
lock.lock();
System.out.println("唤醒时间为:" + System.currentTimeMillis());
condition2.signalAll();
} finally {
lock.unlock();
}
}
}
等待结束时间为:1516675014721
volatile关键字
volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
拥有多个线程之间的可见性,不是不具备同步性(也就是原子性),可以算上是一个轻量级的synchronized,性能要比synchronized强很多,不会造成阻塞这里需要注意,一般volatite用于只钟对于多个线程可见的变量操作,并不能代替synchronized的同步功能。
volatile关键字只具有可见性,没有原子性,要实现原子性建议使用atomic类的系列对象,支持原子性操作(atomic只保证本身方法的原子性,并不保证多次操作的原子性);
在前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。 要解决这个问题,只需要像在本程序中的这样,把该变量声明为volatile(不稳定的)即可,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下各任务间共享的标志都应该加volatile修饰。 Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新的值,使用volatile变量的第二个语义是禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。而使用volatile关键字,那为何说它禁止指令重排序呢?从硬件架构上讲,指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。 但并不是说指令任意重排,CPU需要能正确处理指令依赖情况以保障程序能得出正确的执行结果。使用volatile变量的会禁止它。
ThreadLocal
ThreadLocal用来存储每个线程的私有数据,解决线程间数据的隔离,线程局部变量一个是锁机制进行时间换空间,一个是存储拷贝进行空间换时间。
ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal<T>版本。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
上一篇: Java 设计模式之单例模式
下一篇: 纯css实现照片墙3D效果