一、Java线程生命周期
Java面试题:
线程一共有几种状态,之间是如何切换的?
线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。
新建:就是刚使用new方法,new出来的线程;
就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
新建状态:Thread t1 = new Thread();
这里的新建,仅仅是在JAVA的这种编程语言层面被创建,而在操作系统层面,真正的线程还没有被创建。只有当我们调用了 start() 方法之后,该线程才会被创建出来,进入Runnable状态。只有当我们调用了 start() 方法之后,该线程才会被创建出来。
就绪状态:t1.start()
调用start()方法后,JVM 进程会去创建一个新的线程,而此线程不会马上被 CPU 调度运行,进入Running状态,这里会有一个中间状态,就是Runnable状态,你可以理解为等待被 CPU 调度的状态。
就绪(runnable)状态支持的改变
Runnable状态的线程无法直接进入Blocked状态和Terminated状态的。只有处在Running状态的线程,换句话说,只有获得CPU调度执行权的线程才有资格进入Blocked状态和Terminated状态,Runnable状态的线程要么能被转换成Running状态,要么被意外终止。
运行状态
运行(running)状态支持的改变
当CPU调度发生,并从任务队列中选中了某个Runnable线程时,该线程会进入Running执行状态,并且开始调用run()方法中逻辑代码。
那么处于Running状态的线程能发生哪些状态转变?
被转换成Terminated状态,比如调用 stop() 方法;
被转换成Blocked状态,比如调用了sleep, wait 方法被加入 waitSet 中;
被转换成Blocked状态,如进行 IO 阻塞操作,如查询数据库进入阻塞状态;
被转换成Blocked状态,比如获取某个锁的释放,而被加入该锁的阻塞队列中;
该线程的时间片用完,CPU 再次调度,进入Runnable状态;
线程主动调用 yield 方法,让出 CPU 资源,进入Runnable状态。
阻塞状态
阻塞(Blocked)状态支持的改变
被转换成Terminated状态,比如调用 stop() 方法,或者是 JVM 意外 Crash;
被转换成Runnable状态,阻塞时间结束,比如读取到了数据库的数据后;
完成了指定时间的休眠,进入到Runnable状态;
正在wait中的线程,被其他线程调用notify/notifyAll方法唤醒,进入到Runnable状态;
线程获取到了想要的锁资源,进入Runnable状态;
线程在阻塞状态下被打断,如其他线程调用了interrupt方法,进入到Runnable状态;
终止状态
进入终止(Terminated)几种可能性:
线程正常运行结束,生命周期结束;
线程运行过程中出现意外错误;
JVM 异常结束,所有的线程生命周期均被结束。
二、多线程并发带来的问题
并发编程绕不开同步和通信问题;
多个线程如果需要读写同一个对象,必然会出现同步问题(思考:如果都是读会出现同步问题吗? 比如String 这种 immutable对象)
前面的ThreadByExtend 和 ThreadByRunnable 都是出现了同步问题,即买票总数不超过200张。(看代码)代码
要解决同步问题,首先要理解JMM java内存模型。
三、Java内存模型–JMM
JMM定义了线程和主内存之间的抽象关系,决定一个线程对共享变量的写入何时对另一个线程可见
简单的讲,Java 内存模型将内存分为共享内存和本地内存。共享内存又称为堆内存,指的就是线程之间共享的内存,包含所有的实例域、静态域和数组元素。每个线程都有一个私有的,只对自己可见的内存,称之为本地内存。
共享内存中共享变量虽然由所有的线程共享,但是为了提高效率,线程并不直接使用这些变量,每个线程都会在自己的本地内存中存储一个共享内存的副本,使用这个副本参与运算。由于这个副本的参与,导致了线程之间对共享内存的读写存在可见性问题。
注意:
JMM中的线程本地内存只是一个抽象概念,并没有一个实体叫线程本地内存, 它可能对应硬件中的寄存器、cache、写缓冲区、编译器优化等等 。实际中如何对应的并不重要,JVM规范也不规定这些细节,只需要知道每个线程有自己专属的『独立的工作内存』就好。
四、并发编程三大特性
原子性:
即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
Java中除了long和double之外,其他基础类型的赋值操作都是原子的。
但是 ++ 、-- 操作不是原子的!
为了解决long的赋值不原子的问题,java提供了AtomicLong类型,但没有提供对应的double和float类型,如果这两个类型要保证原子性需要手动解决。
可以想象如果对int i的复制不是原子的话, 可能是先赋值上16位, 后赋值下16位,这样两个线程如果同时赋值,则可能上16位是一个线程赋值,下16位是另一个线程赋值, 会导致数据不一致问题。
可见性:
是指当多个线程访问同一个共享变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
//线程1执行的代码
int i = 1;
i = 100;
//线程2执行的代码
int j = i;
线程1执行i=100这句时,会先把i的初始值加载线程本地缓存中,然后赋值为100,却没有立即写入到主存当中。此时,线程2执行j=i,它会先去主存读取i的值并加载到核2的缓存当中,注意此时内存当中i 的值还是1,那么就会使得j的值为1,而不是100。
顺序性:
即程序执行的顺序按照代码的先后顺序执行。
//线程1:
boolean finished = false;
int temp = 0;
...
temp = 1; //语句1
finished = true; //语句2
//线程2:
while(!finished ){
sleep()
}
add(temp, 10);
从写的代码顺序上看,语句1 是在语句2前面的,但是JVM 在真正执行这段代码的时候并不会保证语句1一定会在语句2 前面执行,这里可能会发生 指令重排序(Instruction Reorder)。
面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的(单线程情形下)。
虽然重排序不会影响单个线程内程序执行的结果,但是多线程就会。
比如上面这个示例代码中, 因为finish=true 和 temp=1操作的对象完全无关, 因此可能会又指令重排让finished=ture先执行,结果线程二就会把temp加10,但线程一又会将temp置为1。
Volatile关键字
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰后,那么就具备了两层语义:
保证一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的,禁止进行指令重排序;
一个线程对 volatile 变量的读,一定能看见在它之前最后一个线程对这个变量的写。
为了实现这些语义,Java 规定:
当一个线程要使用共享内存中的 volatile 变量时,它会直接从主内存中读取,而不使用自己本地内存中的副本。
当一个线程对一个 volatile 变量进行写时,它会将这个共享变量的值刷新到共享内存中。
Volatile可以保证顺序性与可见性,但不能保证对Volatile 变量的操作是原子的,如:
public void add(){
a++;
}
等价于
public void add() {
temp = a;
temp = temp +1;
a = temp;
}
因此在a++操作过程中,可能a的值会被另一个线程修改!
解决方案是用 AtomicInteger 的 getAndIncrement()操作, Atomic类的实现保证这个操作是原子的。 同时配合Volatile关键字
Volatile AtomicInteger a = new AtomicInteger(0);
public void add(){
a.getAndIncrement();
}
示例代码 AtomicThreadByRunnable
package week3.demo;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicThreadByRunnable implements Runnable {
public static volatile AtomicInteger count = new AtomicInteger(0);
public void run() {
for(int i=0;i<50;i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sold "+(i+1)+" total Sold:" + count.incrementAndGet());
}
}
public static void main(String[] argv) throws InterruptedException{
AtomicThreadByRunnable myRunnable = new AtomicThreadByRunnable();
Thread thread1 = new Thread(myRunnable, "Thread1");
Thread thread2 = new Thread(myRunnable, "Thread2");
Thread thread3 = new Thread(myRunnable, "Thread3");
Thread thread4 = new Thread(myRunnable, "Thread4");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread1.join();
thread2.join();
thread3.join();
thread4.join();
}
}
以上代码可以保证多线程买票总数是200张。