java线程相关整理

2019-09-26  本文已影响0人  benz0516

一、实现方式

线程的实现有两种方式,一是继承Thread类,二是实现Runnable接口。

二、状态

1.新建状态

创建了一个线程对象。

2.就绪状态

线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

3.运行状态

就绪状态的线程获取了CPU,执行run()方法。

4.阻塞状态

阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

a.等待阻塞

运行的线程执行wait()方法,JVM会把该线程放入等待池中。

b.同步阻塞

运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

c.其他阻塞

运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

5.死亡状态

线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

三、run和start的区别

当调用start方法的时候,该线程就进入就绪状态。等待CPU进行调度执行,此时还没有真正执行线程。
当调用run方法的时候,是已经被CPU进行调度,执行线程的主要任务。

四、wait,join,sleep,yield, notify,notifyall,synchronized相关释义

1.wait

在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。
唤醒当前对象锁的等待线程使用notify或notifyAll方法,wait() 和notify()必须在synchronized函数或synchronized block中进行调用。

2.sleep

在指定时间内让当前正在执行的线程暂停执行,但不会释放“锁标志”。不推荐使用。sleep()使当前线程进入阻塞状态,在指定时间内不会执行。

3.yield

暂停当前正在执行的线程对象。yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。yield()只能使同优先级或更高优先级的线程有执行的机会。

4.join

等待该线程终止。等待调用join方法的线程结束,再继续执行。如:在main线程中执行t.join(),主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。(join方法会释放this对象的锁)

五、sleep() 与 wait()的区别

1.有关类

这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object 类。

2.有关锁

最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

3.有关资源

sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。

4.有关唤醒

sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。

5.有关使用

wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用

6.有关异常

sleep需要捕获异常,而wait不需要

六、ThreadLocal

1.定义

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

2.适用场景和总结

七、线程安全

1.概念

一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。

当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。

程序执行的顺序按照代码的先后顺序执行。

2.如何解决多线程并发问题

a.锁和同步
b.CAS(compare and swap)

基础类型变量自增(i++)是一种常被误以为是原子操作而实际不是的操作。Java中提供了对应的原子操作类来实现该操作,并保证原子性,其本质是利用了CPU级别的CAS指令。由于是CPU级别的指令,其开销比需要操作系统参与的锁的开销小。AtomicInteger使用方法如下。

AtomicInteger atomicInteger = new AtomicInteger();
for(int b = 0; b < numThreads; b++) {
  new Thread(() -> {
    for(int a = 0; a < iteration; a++) {
      atomicInteger.incrementAndGet();
    }
  }).start();
}

Java提供了volatile关键字来保证可见性。当使用volatile修饰某个变量时,它会保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最新的值。

happens-before原则(先行发生原则)

3.volatile适用场景

volatile适用于不需要保证原子性,但却需要保证可见性的场景。一种典型的使用场景是用它修饰用于停止线程的状态标记。如下所示

boolean isRunning = false;

public void start () {
  new Thread( () -> {
    while(isRunning) {
      someOperation();
    }
  }).start();
}

public void stop () {
  isRunning = false;
}

在这种实现方式下,即使其它线程通过调用stop()方法将isRunning设置为false,循环也不一定会立即结束。可以通过volatile关键字,保证while循环及时得到isRunning最新的状态从而及时停止循环,结束线程。

4.相关问题

问:平时项目中使用锁和synchronized比较多,而很少使用volatile,难道就没有保证可见性?
答:锁和synchronized即可以保证原子性,也可以保证可见性。都是通过保证同一时间只有一个线程执行目标代码段来实现的。

问:锁和synchronized为何能保证可见性?
答:根据JDK 7的Java doc中对concurrent包的说明,一个线程的写结果保证对另外线程的读操作可见,只要该写操作可以由happen-before原则推断出在读操作之前发生。

The results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation. The synchronized and volatile constructs, as well as the Thread.start() and Thread.join() methods, can form happens-before relationships.

问:既然锁和synchronized即可保证原子性也可保证可见性,为何还需要volatile?
答:synchronized和锁需要通过操作系统来仲裁谁获得锁,开销比较高,而volatile开销小很多。因此在只需要保证可见性的条件下,使用volatile的性能要比使用锁和synchronized高得多。

问:既然锁和synchronized可以保证原子性,为什么还需要AtomicInteger这种的类来保证原子操作?
答:锁和synchronized需要通过操作系统来仲裁谁获得锁,开销比较高,而AtomicInteger是通过CPU级的CAS操作来保证原子性,开销比较小。所以使用AtomicInteger的目的还是为了提高性能。

问:还有没有别的办法保证线程安全
答:有。尽可能避免引起非线程安全的条件——共享变量。如果能从设计上避免共享变量的使用,即可避免非线程安全的发生,也就无须通过锁或者synchronized以及volatile解决原子性、可见性和顺序性的问题。

问:synchronized即可修饰非静态方式,也可修饰静态方法,还可修饰代码块,有何区别
答:synchronized修饰非静态同步方法时,锁住的是当前实例;synchronized修饰静态同步方法时,锁住的是该类的Class对象;synchronized修饰静态代码块时,锁住的是synchronized关键字后面括号内的对象。

上一篇 下一篇

猜你喜欢

热点阅读