1. 进程与线程
进程:是操作系统中的各个程序,是资源分配的基本单位,举例:QQ.exe,分配内存、端口号
线程:是执行程序的基本单位,也就是说程序中的代码是由线程执行的,一个进程中包含一个或多个线程,可以并行执行。
1. 多线程的重要性?
多线程是基础,基础到什么程度?基础到如果不会多线程,那么最简单的CRUD都写不好。
具个例子:现在有一个Person类,有name属性,保存时要求name唯一,
这时如果两个用户同时提交且名字一样,那么后台就会有两个线程同时执行 create 方法,最终导致数据库存入两条重复的数据。
或许你会说在数据库表中增加唯一约束,但是如果该表的数据只能逻辑删除,这样就是有问题的。
所以,只有学好多线程、锁,这些东西,才能写好代码。
2. 创建线程的两种方式
/**
* 创建线程的方式:
* 1. 继承 Thread 类,复写 run 方法
* 2. 实现 Runnable 接口,推荐使用,因为java语言规范,不能多继承,但是允许多实现
* 3. Callable,需要被 FutureTask 封装,通过get方法获取返回值,而 FutureTask 本质上还是实现了Runnable
* 4. 线程池,本质上还是第二种
*
*/
class T1 extends Thread {
@Override
public void run() {
System.out.println("这是创建线程的第1种方式");
}
}
class T2 implements Runnable {
@Override
public void run() {
System.out.println("这是创建线程的第2种方式,推荐使用");
}
}
class T3 implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("这是创建线程的Callable方式");
return "thread";
}
}
public class T01_Create_thread {
public static void main(String[] args) {
//1. 继承 Thread 类
new T1().start();
//2. 实现 Runnable 接口
new Thread(new T2()).start();
//3. Callable
FutureTask<String> task = new FutureTask<>(new T3());
new Thread(task).start();
System.out.println(task.get());
}
}
为什么写了4种?
因为网上有种说法,把 Callable和线程池 各自列为一种创建线程的方式,
但是我们需要知道其实它们的在本质上都是第二种
3. 线程状态
该知识点,了解就行,Thread类中有State枚举
1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程创建后,且调用了start()方法。该状态的线程位于可运行线程池中,等待获取CPU的使用权,
此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程等待获取锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(唤醒或打断)。
5. 超时等待(TIMED_WAITING):跟WAITING不同,可在指定时间后自己运行,如果等待时释放锁,超时后阻塞于锁。
6. 终止(TERMINATED):表示该线程已经执行完毕。
状态切换时常用方法
该知识点,了解就行,Thread类中有State枚举
1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程创建后,且调用了start()方法。该状态的线程位于可运行线程池中,等待获取CPU的使用权,
此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程等待获取锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(唤醒或打断)。
5. 超时等待(TIMED_WAITING):跟WAITING不同,可在指定时间后自己运行,如果等待时释放锁,超时后阻塞于锁。
6. 终止(TERMINATED):表示该线程已经执行完毕。
状态切换时常用方法
start() | 启动一个线程 |
run() | 线程需要执行的代码,run方法结束,该线程结束 |
sleep(long millis) | 线程休眠,但不释放锁 |
join() | 等待该线程结束,可以让线程顺序执行 |
wait()/notify()/notifyAll() | wait()使当前线程等待,前提是 必须先获得锁,一般配合synchronized 关键字使用 只有当 notify/notifyAll() 被执行时候,才会唤醒该线程继续执行,直到执行完synchronized 代码块或是再次遇到wait() notify/notifyAll() 的执行只是唤醒等待的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁 |
代码演示
/**
* 工具类
*/
public class ThreadHelper {
public static void sleep(long millisecond) {
try {
Thread.sleep(millisecond);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 状态切换时常用方法
* start()
* 启动一个线程
* run()
* 线程需要执行的代码,run方法结束,该线程结束
* sleep(long millis)
* 线程休眠,但不释放锁
* join()
* 等待指定的线程结束,可以让线程顺序执行
* wait()/notify()/notifyAll()
* wait()使当前线程等待,前提是 必须先获得锁,一般配合synchronized 关键字使用
* 只有当 notify/notifyAll() 被执行时候,才会唤醒该线程继续执行
*
* notify/notifyAll() 的执行只是唤醒等待的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。
* 所以尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁
*/
public class T02_Thread_status02 {
public static void main(String[] args) throws InterruptedException {
waitAndNotify();
}
/**
* wait/notify/notifyAll的使用
*/
private static void waitAndNotify() throws InterruptedException {
Object t = new Object();
new Thread(() -> {
synchronized (t){
try {
System.out.println("线程1开始等待。。。。。。");
t.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程1结束。。。。。。");
}).start();
new Thread(() -> {
synchronized (t){
try {
System.out.println("线程2开始等待。。。。。。");
t.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程2结束。。。。。。");
}).start();
ThreadHelper.sleep(2000);
new Thread(() -> {
synchronized (t){
System.out.println("唤醒线程。。。。。。");
t.notify();//只会唤醒等待线程中的一个,具体是哪一个,跟JVM有关,hotspot是顺序唤醒,唤醒的线程在当前线程释放锁后,继续运行
// t.notifyAll();
ThreadHelper.sleep(2000);
}
}).start();
}
}
特殊情况下的notify
/**
* 特殊情况下,notify会唤醒所有
*/
class T extends Thread {
@Override
public void run() {
synchronized (this){
this.notify();
System.out.println(this.hashCode()+"----"+this);
}
}
}
public class T02_Thread_status03 {
public static void main(String[] args) throws InterruptedException {
new T02_Thread_status03().waitAndNotify();
}
/**
* wait/notify/notifyAll的使用
*/
private void waitAndNotify() throws InterruptedException {
/**
* 1. 锁是一个 线程
*/
T t = new T();
// Thread t = new Thread(() ->{
// synchronized (this){
// System.out.println(this.hashCode()+"----"+this);
// this.notify();
// }
// });
new Thread(() -> {
System.out.println("线程1开始等待。。。。。。");
synchronized (t){
try {
t.wait();//释放锁
System.out.println(t.hashCode()+"----"+t);
System.out.println("线程1结束。。。。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
System.out.println("线程2开始等待。。。。。。");
synchronized (t) {
try {
t.wait();
System.out.println(t.hashCode()+"----"+t);
System.out.println("线程2结束。。。。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
ThreadHelper.sleep(2000);
t.start();
}
}
4. 一些常见问题
1. 多个线程顺序执行
- 使用join方法
- 定义一个共享变量,各个线程根据变量执行
2. 停止线程
停止线程的最好方式是让线程正常结束
- 声明一个变量,设置一个开关
- interrupt方法,interrupt()并不会终止线程!只是将线程的中断标记设为true
-
- 如果线程在阻塞、睡眠、等待,会抛出InterruptedException
/**
* 一些常见问题
*/
public class T03_Other {
public static void main(String[] args) throws InterruptedException {
//1. 多个线程之间顺序执行
Thread t1 = new Thread(() -> System.out.println("线程1执行。。。。。。"));
Thread t2 = new Thread(() -> System.out.println("线程2执行。。。。。。"));
Thread t3 = new Thread(() -> System.out.println("线程3执行。。。。。。"));
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
t3.join();
//
// System.out.println("执行完毕");
//join可以阻塞调用线程的执行(本例中调用线程是主线程),等指定线程执行结束后才继续执行调用线程
/**
* 2. 停止一个线程
* 停止线程的最好方式是让线程正常结束
* a. 在循环中声明一个变量,设置一个开关
* b. interrupt 方法,但它并不会终止线程!只是将线程的中断标记设为true
* 如果线程在阻塞、睡眠、等待,会抛出InterruptedException
*/
// Thread t = new Thread(() -> {
// for (int i = 0; i<10; i++) {
if(Thread.currentThread().isInterrupted()){
break;
}
// System.out.println(i);
// ThreadHelper.sleep(1000);
// }
// });
// t.start();
// Thread.sleep(1000);
// t.interrupt();
// t.isInterrupted();// true:表示当前线程被打断了
// Thread.interrupted();//查询当前线程是否被打断,并重置打断标志,其实里面调用的还是isInterrupted方法
}
}
5. synchronized
1. 字符串加锁
不建议对字符串加锁,字符串比较特殊,一般情况下在内存中只有一份儿,两个线程分别对同一个字符串加锁,非常容易产生阻塞,甚至是死锁。而且如果用法不对,加锁毫无效果。
/**
* 字符串加锁
* 不建议对字符串加锁,字符串比较特殊,一般情况下在内存中只有一份儿,两个线程分别对同一个字符串加锁,非常容易产生阻塞,甚至是死锁。而且如果用法不对,加锁毫无效果。
*/
public class T04_Synchronized {
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
show("feng");
}).start();
new Thread(() -> {
show(new String("feng"));
}).start();
}
public static void show(String str){
synchronized (str){
// synchronized (str.intern()){ // 如果字符串池中存在当前字符串, 就会直接返回当前字符串. 如果没有, 会将字符串放入池中后, 再返回
System.out.println(str);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2. 锁升级过程
待完善
简单过程
- 第一次加锁,偏向锁,记录Thread Id
- 另一个线程来,发现Thread Id 不同,锁升级:轻量级锁,又称 自旋锁
- 自旋一定次数后仍然抢不到锁,升级为重量级锁,线程挂起,等待
轻量级和重量级锁的使用,轻量级自旋时很消耗cpu
如果线程数少,而且运行速度较快,适合轻量级锁,反之使用重量级锁
2. 线程3个特性
1. 可见性
在java中,每一个线程都有一块工作内存,其中存放着主内存中的变量值得拷贝,当线程执行时,它在自己的工作内存区中操作这些变量。
代码:
/**
* 证明工作内存的存在
*/
class Vo implements Runnable {
boolean bool = true;
@Override
public void run() {
System.out.println("start......");
while (bool){
}
System.out.println("end......");
}
}
public class T05_Volatile {
public static void main(String[] args) throws InterruptedException {
Vo vo = new Vo();
new Thread(vo).start();
Thread.sleep(1000L);
vo.bool = false;
}
}
//运行该代码,会发现死循环,“System.out.println("end......");”这句代码永远也不会执行
//这时候只要在bool变量前加上volatile修饰符,程序就能正常结束
volatile 使变量在多个线程中可见,当一个线程修改变量后,强制其他线程到主内存中读取变量值,性能比synchronized强,不会阻塞。但是不具备原子性,不适当的使用,在CPU层面上极有可能造成计算速度降低。
代码:
/**
* 不适当的使用volatile会造成速度降低
*/
class Demo {
long a;
// volatile long a;
}
public class Volatile03 {
public static void main(String[] args) throws InterruptedException {
Demo[] arr = new Demo[]{new Demo(),new Demo()};
long start = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000000; i++) {
//第1个线程修改数组中的第一个元素
arr[0].a = i;
}
});
t1.start();
Thread t2 = new Thread(() -> {
for(int i =0;i<100000000;i++){
//第2个线程修改数组中的第二个元素
arr[1].a = i;
}
});
t2.start();
t1.join();t2.join();
System.out.println(System.currentTimeMillis()-start);
}
以上代码运行时间:220毫秒左右,
对代码进行修改,把变量a加上 volatile,运行时间:3000毫秒左右。
想知道原因,得先弄明白以下几个东西:
CPU缓存
工作内存 本质上就是 CPU缓存 ,是 CPU与内存之间的临时数据区
为什么需要CPU缓存?
- 解决CPU运行速度与内存读写速度不匹配的矛盾——缓存的速度比内存的速度快多了。
- CPU往往需要重复处理相同的数据、重复执行相同的指令,如果这部分数据、指令CPU能在CPU缓存中找到,CPU就不需要从内存或硬盘中再读取数据、指令,从而提高运行速度。
CPU缓存分为3级:L1一级缓存、L2二级缓存、L3三级缓存,它们的作用都是作为CPU与主内存之间的高速数据缓冲区,L1最靠近CPU核心;L2其次;L3再次。
速度方面:L1最快、L2次快、L3最慢;
大小方面:L1最小、L2较大、L3最大。
CPU会先在L1中寻找需要的数据,找不到再去L2,还找不到再去L3,L3都没有那就只能去主内存找了。
一级缓存其实还分为一级数据缓存(Data Cache,L1d-Cache)和一级指令缓存(Instruction Cache,l1i-Cache),分别用于存放数据及指令,两者可同时被CPU访问,减少了CPU多核心、多线程争用缓存造成的冲突,提高处理器性能。