(五)多线程:线程同步

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讲义-李刚》一书中多线程章节。截取重要知识点作为笔记记录,方便自己回顾。

猜你喜欢

转载自www.cnblogs.com/everyingo/p/12795664.html