Java 多线程同步以及线程之间的协作

一. 线程状态

Java 线程有下列五种状态:

1. 初始状态(New)
线程对象被创建后,就进入了初始状态, 此时线程会被分配必须的系统资源, 并进行了初始化操作, 代表该线程有资格获取CPU的时间片了;

2. 就绪状态(Runnable)
线程对象被创建后,其它线程调用了该线程的start()方法,从而启动该线程; 处于就绪状态的线程,随时可能被CPU调度执行, 也就是说此时线程可运行也可以不运行, 争取到CPU时间片就运行,否则就不运行;

3. 运行状态(Running)
可运行状态的线程获得了CPU时间片,执行了该线程的程序代码,线程只能从就绪状态进入到运行状态;

4. 阻塞状态(Blocked)
阻塞状态表示线程被锁阻塞, 也就是说线程此时能够运行,但是某个条件阻止了它的运行, 当线程处于阻塞状态时, 线程调度器将忽略该线程, 不会分配给它CPU时间片, 直到该线程重新进入就绪状态是线程放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

阻塞的情况分三种:
(1) 调用了sleep() 方法使线程进入休眠状态, 在这种情况下, 线程在指定的时间内不会运行;
(2) 通过调用wait()方法使线程挂起, 直到线程得到了notify()或者notifyAll()的消息,线程才会重新进入就绪状态;
(3) 线程试图在某个对象上调用同步方法, 但由于同步锁被其他线程持有,导致当前线程在获取synchronized同步锁失败, 它会进入同步阻塞状态;
(4) 线程在等待某个输入/输出完成;

5. 死亡状态(Dead)
线程执行完了run方法, 或者因异常退出了run()方法,线程就是死亡。

二. 线程状态的转化

Java 多线程同步以及线程之间的协作_第1张图片
引用网上的某位大神的图片, 图片很清晰的展示了java线程的几种状态的转化;

二. 多线程同步

1. synchronized关键字

当任务要执行被synchronized关键字被保护的代码片段时,它将检查锁是否可用, 如果可用则获取锁, 执行代码, 然后再释放锁;

(1) 修饰方法
synchronized关键字修饰的方法

public synchronized void method() {
   // do something
}

由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

注: synchronized关键字可以修饰静态方法,如果调用该静态方法,将会锁住整个类

关键字synchronized取得的锁是对象锁,而不是把一段代码或方法当作锁,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁,那么其他线程只能呈等待状态, 知道正在拿到该方法所属对象的锁;

锁重入:关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个synchronized方法的内部调用本类的其他synchronized方法块时,是永远可以得到锁的。如果不可锁重入的话,就会造成死锁。当存在父子类继承关系时,子类可以通过“可重入锁”调用父类的同步方法。

当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
同步不能被继承,如果子类想实现同步,还得在子类的方法中添加synchronized关键字。

(2) 修饰代码块
synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步;

(3) 修饰静态方法

public synchronized static void method() {
   // do something
}

synchronized关键字加到static静态方法上是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁。要注意Class锁可以对类的所有对象起作用。而同步synchronized(class)代码块与synchronized static方法的作用一样。

(4) 修饰类
作用的是静态方法所在类的所有对象

class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // do something
      }
   }
}

synchronized优缺点:

同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
在定义接口方法时不能使用synchronized关键字。
优点:
使用synchronized关键字解决多线程资源共享时, 需要写的代码量很少, 并且出现错误的的可能性也会降低;
缺点:
使用synchronized修饰方法会在一定程度上影响性能,比如A线程调用同步方法执行一个耗时的任务,那么B线程则必须等待较长时间,影响性能; 在这样的情况下更好的做法是使用synchronized同步语句块来解决, 减少同步的内容;这样方法中不在synchronized块中的代码就是异步执行,在synchronized中的代码就是同步执行。

2. Lock

Lock 是Java SE5提供的一种显式的互斥机制, Lock对象必须显式地创建, 锁定和释放;

Lock是一个接口, 定义如下:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long var1, TimeUnit var3) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

Lock提供多种方式获取锁,有两个接口:LockReadWriteLock; 而这两个接口的实现类有ReentrantLockReentrantReadWriteLock

Locksynchronized不同点:

  1. Lock是一个接口,而synchronized是Java中的关键字;

  2. 由于Lock的加锁和释放锁是手动的,所以Lock的操作与synchronized相比,灵活性更高;

  3. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  4. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  5. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到;

  6. Lock可以提高多个线程进行读操作的效率。
    从性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说在具体使用时要根据适当情况选择。

Lock的使用

public class LockDemo {

    private Lock mLock = new ReentrantLock();

    private void testLock(Thread thread){
        mLock.lock();
        System.out.println(thread.getName()+"获取了lock");
        try {
            //模拟耗时任务
            thread.sleep(2000);
        } catch (Exception e) {
        } finally {
            System.out.println(thread.getName()+"释放了lock");
            mLock.unlock();
        }
    }
    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lockDemo.testLock(Thread.currentThread());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                lockDemo.testLock(Thread.currentThread());
            }
        }).start();
    }
}

执行结果: 首先Thread-1显拿到了锁, 只有当Thread-1释放了锁之后Thread-0才可以拿到锁;
Java 多线程同步以及线程之间的协作_第2张图片

3. volatile关键字

为了说清楚volatile关键字, 得先了解一下处理器在读取数据的特点:

为了提高程序的运行速度, 处理器不会每次直接从系统内存中读取数据, 而是先将系统内存数据读取到内部缓存, 然后再进行操作, 但操作完不会立即写到系统内存, 写入具体时间不确定, 就算写回到系统内存, 在多处理器下, 其他处理器缓存的值还是旧值, 用这个旧值进行运行操作就会出问题; 所以在多核处理器下, 必须实现缓存一致, 每个处理器在读取数据的时候都要通过主线上传播过来的数据检查自己内部缓存的数据是不是过期了, 如果过期了就将内部缓存数据改成无效, 此时该处理器对这个数据进行写操作时就会重新从主存中把数据读取到处理器的内部缓存中, 这样就保证了缓存一致;

volatile关键字修饰的变量有两个作用:
1. 将当前处理器的数据写回到系统内存;
2. 将其他处理器中缓存的该数据的改成无效, 使其重新在主存中读取;

可见性

所以说volatile 关键字作用是保证变量在多个线程间可见, 这种特性也叫可见性; volatile修饰的变量可以保证下一此读取操作会在前一个写操作之后发生

原子性

volatile关键字有一个最致命的缺点:不支持原子性。

什么叫原子性?

原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败, 即原子操作是不能被线程调度机制中断的操作, 一旦操作开始, 那么它一定可以在切换到其他线程之前执行完成;

例如: Java中long赋值不是原子操作,因为先写32位,再写后32位,分两步操作, 这样就会出现线程不安全的问题, 加上volatile修饰就线程安全了,

private volatile long number = 100;

当然我们平时不会这么做,因为java提供了更方便了类AtomicLong

synchronized和volatile比较:

关键字volatile是线程同步的轻量级实现,性能比synchronized好,且

1.volatile只能修饰变量,synchronized可修饰方法和代码块;
2. 多线程访问volatile不会发生阻塞,synchronized会出现阻塞;
3. volatile能保证数据可见性,不保证原子性;synchronized可以保证原子性,也可以间接保证可见性;
4. volatile解决的是变量在多个线程间的可见性,synchronized解决的是多个线程访问资源的同步性。

java提供的多线程同步机制还有CountDownLatch, CyclicBarrier, Semaphore, Exchanger, 后续再总结;

你可能感兴趣的:(Java)