目录
1. 线程的五种状态(操作系统层面)?
2.线程的状态(JAVA层面)?
3.. 线程相关的基本方法?
4. wait()和sleep()的区别?
5.sleep()方法和yield()方法区别?
6.synchronized和Lock(ReentrantLock)有什么区别?
7.同步锁、死锁、乐观锁、悲观锁?
8.synchronized原理进阶(锁膨胀)?
这是从操作系统层面来描述的:
1. New -> 新建状态:创建了线程对象,但是并没有调用该对象的start方法,这时线程处于创建状态。
2. Runnable -> 就绪状态:当调用了线程对象的start方法之后,该线程就进入了就绪状态。但是此时操作系统的任务调度器程序还没有把该线程设置为当前线程,所以该线程此时处于就绪状态。
3. Running -> 运行状态:线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run方法当中的代码。
4. 阻塞状态:阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(1) Waiting -> 等待状态:通过调用线程的wait()方法,让线程等待某工作的完成。
(2) Timed_Waiting -> 超时等待状态:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
(3) Blocked -> 同步阻塞状态:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
5. Terminated -> 终止状态:线程执行完了或者因异常退出了run()方法时,该线程就会结束生命周期,进入到终止状态。
线程相关的基本方法有wait(),notify(),notifyAll(),sleep(),join(),yield()等。
1.线程等待(wait())
调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回。需要注意的是调用wait()方法后,会释放对象的锁。因此wait()方法一般需要绑定synchronized同步关键字,一起在同步方法或者同步代码块中使用。
2.线程睡眠(sleep())
sleep()方法会导致当前线程休眠,与wait()方法不同的是,sleep()方法不会释放当前占有的锁。sleep(long)方法会导致线程进入TIMED-WATING状态,而wait()方法会导致当前线程进入WATING状态。
3.线程让步(yield())
yield()方法会使当前线程让出CPU执行的时间片,与其他线程一起重新竞争 CPU的时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 的时间片。但这又不是绝对的,有的操作系统对线程的优先级并不敏感。
4.线程中断(interrupt())
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。但是这个线程本身并不会因此而改变状态(如阻塞,终止等)。
5.等待其他线程终止(join())
join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转为阻塞状态。等到另一个线程结束,当前线程再由阻塞状态变为就绪状态,然后等待操作系统的任务调度器组件给其分配CPU的时间片。
6.线程唤醒(notify()和notifyAll())
Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争,来获取CPU的时间片。类似的方法还有notifyAll(),唤醒在此监视器上等待的所有线程。
1. 来自不同的类
wait():来自Object类;
sleep():来自Thread类;
2.关于锁的释放:
wait():在等待的过程中会释放锁;
sleep():在等待的过程中不会释放锁
3.使用的范围:
wait():必须在同步代码块中使用;
sleep():可以在任何地方使用;
4.是否需要捕获异常
wait():不需要捕获异常;
sleep():需要捕获异常;
sleep()方法:
1. 调用sleep()方法会让当前线程从Running状态进入Timed_Waiting状态(阻塞)
2. 其它线程可以使用interrupt()方法来打断正在睡眠的线程,这时sleep()方法会抛出InterruptedException异常。
3. 睡眠结束后的线程未必会立刻得到执行。
4. 建议使用TimeUnit类的sleep()方法来代替Thread类的sleep()方法来获得更好的可读性。
yield()方法:
1. 调用yield()方法会让当前线程从Running状态进入到Runnable状态。然后CPU可以调度执行其它线程。
2. yield()方法具体的实现依赖于操作系统的任务调度器组件。
同步锁:
当多个线程同时访问同一个共享数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据,来保证原子性,可见性以及有序性等条件。Java中可以使用synchronized关键字来取得一个对象的同步锁。
死锁:
何为死锁,就是两个或者多个线程同时被阻塞住,它们中的一个线程或者全部线程都在等待某个资源被释放。(比如:t1线程获得到了A对象锁,接下来想获取B对象的锁,t2线程获得到了B对象锁,接下来想获取A对象的锁,它们都持有对方想要的资源,但是又不肯释放自己所占有的资源,所以双方都进入到了阻塞状态。比如著名的哲学家就餐问题)。
乐观锁:
总是假设最好的情况,每次去拿数据的时候都会认为别人不会去修改,所以不会上锁。但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,就像数据库提供的类似于write_conditio机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS来实现的。
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都会认为别人会修改,所以每次在拿数据的时候都会上锁。这样别人想拿这个数据就会被阻塞住,直到它拿到锁为止(共享资源每次只给一个线程使用,其它的线程会被阻塞住,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized同步锁关键字和ReentrantLock可重入锁等独占锁就是悲观锁思想的实现。
首先:Java HotSpot虚拟机中,每个对象都有对象头(包括class指针和 Mark Word)。Mark Word平时存储这个对象的哈希码、分代年龄。当加锁时,这些信息就根据情况被替换为标记位、线程锁记录指针、重量级锁指针、线程ID等内容。
轻量级锁:轻量级锁的使用场景:如果一个对象(共享变量)虽然有多个线程要访问,但加锁的时间是错开的(也就是说没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是synchronized。
重量级锁:如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
自旋优化:重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
偏向锁:轻量级锁在没有竞争时(就自己这个线程),每次重入时仍然需要执行 CAS操作。Java 6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。
锁消除:JVM会进行代码的逃逸分析,例如某个加锁对象是方法内的局部变量,不会被其它线程所访问到。这时候就会被即时编译器(JIT)忽略掉所有同步操作。