一、线程
1、进程与线程
进程:程序的一次执行过程,是系统运行程序的基本单位。启动一个main函数就是启动一个进程
线程:一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程,他们有自己独立的程序计数器、虚拟机栈和本地方法栈,但共享进程的堆和方法区资源
2、线程创建
其实main方法的执行,就已经是运行了多个线程了,包括(1)分发处理发送给JVM信号的线程;(2)调用对象finalize方法的线程;(3)清楚refrence的线程;(4)main线程
那么如何在main中再新建线程,有以下4种方法:
(1)通过继承Thread类,重写run方法;
(2)通过实现runable接口;
(3)通过实现callable接口,需要配合使用线程池
//全部采用匿名内部类的方式实现,也可以单独写一个类继承自Thread类或者实现Runnable接口
public static void main(String[] args) {
//1.继承Thread
Thread thread1 = new Thread() {
@Override
public void run() {
System.out.println("继承Thread");
super.run();
}
};
//2.实现runable接口
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("实现runable接口");
}
});
//3.实现callable接口,配合线程池使用
ExecutorService service = Executors.newSingleThreadExecutor();
Future<String> future = service.submit(new Callable() {
@Override
public String call() throws Exception {
return "实现Callable接口";
}
});
thread1.start();
thread2.start();
try {
String result = future.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
结果:
优缺点对比:
(1)继承Thread类的方式写法比较简单。但Thread是类,只能被继承,当一个类要实现多线程时,如果继承了Thread类,就不能在继承别的类了。并且,thread.start()只能同时调用一次,所谓的多线程,也只是在主线程以外,建立一个线程。
(2)Runnable、Callable接口可以多实现,并且可以启动多个相同的线程处理一个数据,,,,如果需要访问当前线程,必须使用Thread.currentThread()方法
如果用:
报错:
但是实现Runnable接口的thread2可以多个同时用:
结果:
(3)Callable接口要实现的方法是T call() throws Exception,不是void run(),注意可以有返回值,也可以抛出异常(重写Runnable的run方法不能有异常出现)
最重要的是:运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务的执行情况,可取消任务的执行,还可获取执行结果
3、生命周期和状态转换
刻意对线程加锁,而不是抢占资源时,线程会进入WAITING或者TIMED_WAITING状态,而不是BLOCKED状态
4、守护线程
守护线程是一种特殊的线程,是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程,JIT线程就可以理解守护线程。
与之对应的是用户线程,即工作线程,即完成某个业务的线程,当用户线程全部结束后,意味着没有线程需要守护,那么守护线程就会退出,JVM跟着退出
public static void main(String[] args) {
Thread daemonThread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
System.out.println("i am alive");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("finally block");
}
}
}
});
daemonThread.setDaemon(true);
daemonThread.start();
//确保main线程结束前能给daemonThread能够分到时间片
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
结果:
分析:
(1)要将线程设置为守护线程,需要在start()之前调用setDaemon(true);如果放在start之后调用,会报异常,但是程序不会停止,只会当做用户线程执行。
(2)main(用户)线程结束后,没有线程需要守护,守护线程自动退出。
(3)守护线程在退出时不会执行finally块,因为没有用户线程在运行,无论守护线程处于什么状态(阻塞、运行。。。),守护线程直接被kill掉,不再执行任何语句;故释放资源的操作等不要放在finally块中。
5、线程的中断
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
例如:Thread.sleep()方法为什么会抛出InterruptedException,就是因为sleep过程中,一旦其他线程调用了这个线程的interrupt()方法,该线程就会被中断,那就会抛出这个异常。
二、多线程
1、并发VS并行
并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。
实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。
2、使用多线程的优缺点
优点:
(1)充分利用多核CPU的计算能力,提高CPU使用率
(2)异步执行任务
(3)并行计算提升应用性能
缺点:
(1)频繁的上下文切换:CPU分配给各个线程的时间片非常短,CPU通过不断切换线程来让用户觉得是多线程同时运行,但切换线程会损耗性能(保存当前状态以便恢复)
(2)会有线程安全问题
3、线程间的协作
(1)join():在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
(2)wait() notify() notifyAll():
调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
wait() 挂起期间,线程会释放锁
它们都是 Object类 的方法
(3)await() signal() signalAll():
JUC包中的Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活
Condition condition = lock.newCondition();
condition.await();
condition.signalAll();
(4)Thread.sleep()
与Object.wait()比较:
共同点:都可以暂停线程
区别:
a. Object.wait()释放锁,Thread.sleep()没有释放锁
b. Object.wait() 通常被用于线程间交互/通信,Thread.sleep()通常被用于暂停执行。
c. Object.wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 Object.notify()或者 Object.notifyAll() 方法。Thread.sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 Object.wait(long timeout) 超时后线程会自动苏醒,毕竟Thread也是一个Object,也有wait方法。
d. Thread.sleep() 是 Thread 类的静态本地方法,Object.wait() 则是 Object 类的本地方法
sleep() 方法定义在 Thread 中原因:只是让线程暂停,不涉及对象的操作
wait()方法定义在Object中原因:目的是释放线程占用的锁,而锁其实是对象锁,可以不涉及线程
4、线程不安全示例:
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
ThreadUnsafeExample example = new ThreadUnsafeExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
public class ThreadUnsafeExample {
private int cnt = 0;
public void add() {
cnt++;
}
public int get() {
return cnt;
}
}
最终执行结果一定小于1000
5、线程不安全根本原因
(1)可见性:CPU缓存引起
一个线程对共享变量的修改,另外一个线程能够立刻看到。
(2)原子性:分时复用引起
一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
(3)有序性:指令重排序引起
在执行程序时为了提高性能,编译器和处理器常常会对指令按如下顺序做重排序
a.编译器优化的重排序:在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
b.指令级并行的重排序:处理器采用指令级并行技术将多条语句重叠执行(改变语句对应机器指令的执行顺序),前提是不存在数据依赖性
c.内存系统的重排序:处理器使用缓存和读写缓冲区,使得加载和存储操作看上去是乱序执行的
解决:JMM编译器重排序规则会禁止特定类型的编译器重排序;添加内存屏障指令禁止特定的处理器指令重排序,可以使用volatile
6、如何解决并发问题
(1)通过关键字synchronized、volatile、final解决
synchronized解决原子性、可见性、有序性
volatile解决可见性
final使得对象不可变
(2)happens-before原则:解决有序性
八大原则
1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
5. start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
7. 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
8. 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
7、线程安全的实现方法
(1)互斥同步
使用synchronized或reentrantlock加锁
(2)非阻塞同步
CAS、Atomic原子操作,其实Atomic底层也是CAS
(3)无同步
使用线程局部变量、ThreadLocal来实现变量的线程隔离,即不存在多线程的情况