1.线程安全问题
多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
多个线程在操作共享的数据(读写操作),一条线程对共享数据的修改导致其他线程对数据的判断出错(共享数据未修改之前就已出判断)而做出的错误处理,最终产生错误的结果,称之为线程不安全。
2.同步代码块
Java的多线程支持引入同步监视器来解决线程安全问题,使用同步监视器的通用方法就是同步代码块。
synchronized(obj) {
...
//此处代码就是代码同步快
}
synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行之前,必须先获得对同步监视器的锁定。
注意:任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码执行完成后,该线程会释放对该同步监视器的锁定。
推荐:通常推荐使用可能被并发访问的共享资源充当同步监视器。
3.同步方法
同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于synchronized修饰的实例方法(非static方法)而言,无须显示指定同步监视器,同步方法的同步监视器是this,也就是调用改方法的对象。
通过使用同步方法可以非常简单的实现线程安全的类,线程安全的类具有如下特征:
- 该类的对象可以被多个线程安全地访问
- 每个线程调用该对象地任意方法之后都可以得到正确结果。
- 每个线程调用该对象地任意方法只有,该对象状态依然保持合理状态。
为了减少线程安全所带来的负面影响,程序可以采用如下策略:
- 不要对线程安全类的所有方法都进行同步,只有那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。
- 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全和先线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。如:StringBuilder和StringBuffer
4.释放同步监视器的锁定
任何线程进入同步代码块,同步方法之前,必须先获得对同步监视器的锁定,程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定:
- 当前线程的同步方法,同步代码块执行结束,当前线程即释放同步监视器。
- 当前线程在同步代码块,同步方法中遇到break,return终止了该代码块,该方法的继续执行,当前线程将会释放同步监视器。
- 当前线程在同步代码块,同步方法中出现了未处理的Error或Exception,导致了该代码块,该同步方法异常结束时,当前线程将会释放同步监视器。
- 当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
如下情况,线程不会释放同步监视器:
- 线程执行同步代码块或同步方法时,线程调用了Thread.sleep(),Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。当然,程序应该尽量避免使用suspend()和resume()方法来控制线程。
5.同步锁(Lock)
从Java5开始,Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock()对象充当。
Lock提供了比synchronized方法和synchronized代码块更广泛的锁操作,Lock允许实现更灵活的结构,可以具有很大的属性,并且支持多个相关的Condition对象。
Lock是控制多个线程对共享i元进行访问的工具。通常,锁提供了对共享资源的独立访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
在实现线程安全的控制中,比较常用的是RenntrantLock(可重入锁)。使用该Lock可以显式加锁,释放锁。
public class ReentrantLockClass {
private final ReentrantLock lock = new ReentrantLock();
public void m() {
//加锁
lock.lock();
try {
//需要保证线程安全的代码
// method code
} finally {
//释放锁
lock.unlock();
}
}
}
Lock提供了同步方法和同步代码没有的其他功能,包括用户非块结构的tryLock()方法,以及试图获取可中断锁的lockInterruptibly()方法。
6.死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只有所有线程处于阻塞状态,无法继续。
示例代码:
public class A {
public synchronized void a1(B b) {
System.out.println("当前线程:" + Thread.currentThread().getName() + ",进入A实例a1()方法");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程:" + Thread.currentThread().getName() + ",企图调用B实例b2()方法");
b.b2();
}
public synchronized void a2() {
System.out.println("当前线程:" + Thread.currentThread().getName() + ",进入A实例a2()方法");
}
}
public class B {
public synchronized void b1(A a) {
System.out.println("当前线程:" + Thread.currentThread().getName() + ",进入B实例b1()方法");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程:" + Thread.currentThread().getName() + ",企图调用A实例a2()方法");
a.a2();
}
public synchronized void b2() {
System.out.println("当前线程:" + Thread.currentThread().getName() + ",进入B实例b2()方法");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
a.a1(b);
}
@Override
public void run() {
b.b1(a);
}
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
new Thread(deadLock).start();
deadLock.init();
}
}
程序阻塞,输出结果:
当前线程:main,进入A实例a1()方法
当前线程:Thread-0,进入B实例b1()方法
当前线程:main,企图调用B实例b2()方法
当前线程:Thread-0,企图调用A实例a2()方法
注意:由于Thread类得suspend()方法也很容易导致死锁,所以Java不在推荐使用该方法来暂停线程的执行。
文章内容均取自《疯狂Java讲义-李刚》一书中多线程章节。截取重要知识点作为笔记记录,方便自己回顾。