2 线程的同步机制
2.1. 线程安全与非线程安全
1)线程安全与非线程安全
非线程安全是当多个线程访问同一个对象汇总的成员变量时产生的,产生的后果就是脏读,就是指读到的数据是被更改过的。而线程安全就是对获取成员变量的值经过同步处理,不会再出现脏读的现象。
2)线程不安全的产生条件
a.存在多个线程
b.多个线程共享同一个资源
c.对共享资源进行非原子性操作
3)线程安全的特征
1.局部变量是线程安全的,不存在线程安全问题,这是由于局部变量时私有的特征决定的。
样例程序解释:
public class LocalVariable {
public static void main(String[] args) {
LocalVaribleService localVaribleService = new LocalVaribleService();
Thread threadA = new LocalVaribleThreadA(localVaribleService);
threadA.start();
Thread threadB = new LocalVaribleThreadB(localVaribleService);
threadB.start();
}
}
class LocalVaribleService{
public void add(String name){
int num = 0; // 作为add方法内的局部变量
if("A".equals(name)){
num = 1;
System.out.println("A set over");
try {
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}else if("B".equals(name)) {
num = 2;
System.out.println("B set over");
try {
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("名称为" + name + "的num值为" + num);
}
}
class LocalVaribleThreadA extends Thread{
private LocalVaribleService localVaribleService;
public LocalVaribleThreadA(LocalVaribleService localVaribleService){
this.localVaribleService = localVaribleService;
}
@Override
public void run(){
this.localVaribleService.add("A");
}
}
class LocalVaribleThreadB extends Thread{
private LocalVaribleService localVaribleService;
public LocalVaribleThreadB(LocalVaribleService localVaribleService){
this.localVaribleService = localVaribleService;
}
@Override
public void run(){
this.localVaribleService.add("B");
}
}
2.成员变量不是线程安全的,如果有两个线程同时操作业务对象中的成员变量,可能会产生非线程安全问题。解决方法:在方法前使用关键字synchronized进行修饰。
样例程序解释:
public class MemberVariable {
public static void main(String[] args) {
MemberVaribleService memberVaribleService = new MemberVaribleService();
Thread threadA = new MemberVaribleThreadA(memberVaribleService);
threadA.start();
Thread threadB = new MemberVaribleThreadB(memberVaribleService);
threadB.start();
}
}
class MemberVaribleService{
private int num = 0; // 作为成员变量
public void add(String name){
if("A".equals(name)){
num = 1;
System.out.println("A set over");
try {
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}else if("B".equals(name)) {
num = 2;
System.out.println("B set over");
try {
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("名称为" + name + "的num值为" + num);
}
}
class MemberVaribleThreadA extends Thread{
private MemberVaribleService memberVaribleService;
public MemberVaribleThreadA(MemberVaribleService memberVaribleService){
this.memberVaribleService = memberVaribleService;
}
@Override
public void run(){
this.memberVaribleService.add("A");
}
}
class MemberVaribleThreadB extends Thread{
private MemberVaribleService memberVaribleService;
public MemberVaribleThreadB(MemberVaribleService memberVaribleService){
this.memberVaribleService = memberVaribleService;
}
@Override
public void run(){
this.memberVaribleService.add("B");
}
}
2.2. 内置锁与显示锁
1)锁的分类
多线程编程同步时有可能会用到锁,可以分为内置锁(sychronized)和显示锁(例如ReentrantLock)
2)内置锁的对象锁
1、sychronized 作用于实例方法时,锁对象是this,也就是所在类的实例对象
2、sychronized 作用于静态方法时,锁对象是Class对象
3、sychronized 作用于代码块时,锁对象是sychronized(object)中的object
3)内置锁的特性
a.内置锁的互斥。内置锁是一个互斥锁,不仅读写互斥并且读读也互斥,最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁。
b.内置锁是可重入锁。如果已经获取了一个锁对象,在还没释放时又要执行该锁对象的另一个代码块或方法,则不需要再次给这个对象加锁就可以直接执行。
c.内置锁自动释放:在多线程中,当多个线程访问同一对象时,当一个线程先运行某synchronized关键字修饰的方法出现异常时,该线程持有的锁会自动释放,如果该异常没有被捕获或抛出,则该线程停止运行,但是其他线程会继续运行,因停止线程获得的对象锁被自动释放,所以其他线程会获得对象锁。
d.内置锁是非公平锁。线程在竞争synchronized锁时并不遵循先到等待队列就先获得锁,如果一个线程来请求锁,刚好该锁被释放了,那么这个线程可能会跳过在等待队列中的其它线程直接获得该锁。
e. synchronized关键字不能继承。父类方法中加了synchronized,在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,需要重新加锁。
4)内置锁与显示锁的区别
a.锁的释放。内置锁只要运行到同步代码块之外就会自动释放锁;而显示锁必须调用unlock方法才能释放锁。
b.公平性。内置锁不可以选择公平策略,只能是不公平锁; 显示锁可以指定公平策略,默认为不公平锁。
c.可中断申请。 内置锁不可中断申请,在申请锁时被其它线程持有,那么当前线程后挂起,挂起期间不可中断;显示锁提供可中断申请,例如lock.lockInterruptibly(),在申请锁的过程中如果当前线程被中断, 将抛出InterruptedException异常。
d.可尝试申请、可定时早请。 内置锁不提供这种特性;显示锁提供尝试型申请方法,例如Lock.tryLock方法。
e.是否可以精确唤醒特定线程。内置锁的notify或notifyAll方法唤醒在其上等待的线程,但无法指定特定线程;显示锁可以通过Condition对象(由显示锁派生出来),调用Condition.singal或Condition.singalAll方法可以唤醒在该Condition对象上等待的线程,以此来唤醒指定线程。
总结:内置锁够解决大部分需要的场景,当需要额外的灵活时才考虑显示锁,比如公平、可中断、可尝试、可定时、可唤醒特定线程等场景。