一、线程创建
Java中常见创建线程有三种,常见的如下两类
1.1 继承Threa类
Java
public class MyThread extends Thread{
@Override
public void run(){
System.out.println("MyThread is running");
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
1.2 实现Runnable接口
由于继承Thread类后无法继承其它类,因此实现Runnable接口是常见的线程创建方式。Runnable接口只有1个抽象方法run,因此可以直接使用lambda表达式。
Java
public static void main(String[] args) {
Thread t1 = new Thread(()->{
System.out.println("Thread 1 is running");
});
t1.start();
}
二、常用方法
2.1 sleep
sleep是让当前执行的线程让出cpu占用权n毫秒
Java
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
System.out.println("Thread 1 is running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 is ending");
});
t1.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main thread is running");
}
- main线程先执行,走到了t1线程,t1和main同时抢占cpu,然后main被休眠了3s。
- t1线程执行thread1 running,此时t1自己线程也被休眠1s,此时main线程和t1都在休眠。
- t1后,抢占cpu,输出Thread 1 is ending
- main最后才醒来,抢到cpu,输出main thread is running
2.2 join
join让当前线程暂停直到目标线程结束后。如下代码,如果没有t1.join方法,最后的输出肯定是Thread 1 is ending。但t1.join让main线程暂停,必须等t1线程执行结束才能继续,所以输出是
- Thread 1 is running
- Thread 1 is ending
- main thread is running
Java
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
System.out.println("Thread 1 is running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 is ending");
});
t1.start();
t1.join();
System.out.println("main thread is running");
}
2.3 interrupt
interrupt给目标线程贴请停标签;如果对方阻塞会立刻醒,其它情况需要isInterrupted方法判断是否有停止标签。
休眠被唤醒
Java
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
System.out.println("Thread 1 is running");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("Thread 1 is interrupted");
}
System.out.println("Thread 1 is ending");
});
t1.start();
t1.interrupt();
System.out.println("main thread is running");
}
- t1线程在执行时,interrupt告诉t1你需要暂停下,此时t1如果刚好在sleep休眠时,立马被唤醒走到catch,输出Thread 1 is interrupted。
- t1与main线程继续互相抢占cpu。也就是Thread 1 is ending与main thread is running输出顺序随机。
其它情况
线程被interrupt告知暂停时,如果线程刚好休眠会被唤醒进入catch。但是如果线程是其它状态,必须使用isInterrupted获取状态来处理
Java
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
System.out.println("Thread 1 is running");
for(int i=0;i<10000;i++){
if(Thread.currentThread().isInterrupted()){
break;
}
}
System.out.println("Thread 1 is ending");
});
t1.start();
t1.interrupt();
Thread.sleep(2000);
System.out.println("main thread is running");
}
- t1和main线程在互相抢占cpu执行,t1被标记暂停了,main线程睡眠了。
- 此时大概率t1线程在跑任务,输出Thread 1 is running。
- t1继续在执行for循环时,判断状态发现自己被标记暂停了,立马退出,执行Thread 1 is ending
- 然后main线程醒来后,执行main thread is running。
2.4 volatile
volatile保证可见性:我写完后,别人能读取到新值。但是它还是无法解决并发写的问题。
Java
public class VolatileStop {
static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long n = 0;
while (running) {
n++;
}
System.out.println("worker 看到 false,退出 n=" + n);
});
worker.start();
Thread.sleep(100);
running = false;
worker.join();
System.out.println("main 结束");
}
}
三、同步与锁
同步与锁是并发编程非常重要环节,首先需要掌握3个核心api。
3.1 synchronized
3.1 概念
锁可以理解成钥匙,synchronized (x)就是多个线程去抢占这个x锁,每次只能有1个线程能抢到,方法执行结束后后释放锁,没有抢到的线程无法进入方法内。
javascript
synchronized(x){
//do
}
3.2 条件
成功的锁必须满足两个条件
- 多线程抢的必须是同一个对象(同一把钥匙)
- 保护的是同一份共享数据
锁代码块
线程t1和t2共享变量count,而且锁的作用对象都是lock。
scss
private static int count = 0;
private static Object lock = new Object();
public static void increment(){
synchronized(lock){
for(int i=0;i<10000;i++){
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
increment();
});
Thread t2 = new Thread(()->{
increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
2. 锁函数
这里t1和t2线程的锁的对象是static的Class类上,二者还是同一个对象,所以依旧正常。
Java
private static int count = 0;
private static Object lock = new Object();
public static synchronized void increment(){
for(int i=0;i<10000;i++){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
increment();
});
Thread t2 = new Thread(()->{
increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
3. 错误锁
多线程时,锁的对象明显地址是不同的,无效锁。
Java
synchronized (new Object()) {
count++;
}
3.2 wait
wait和sleep有本质差异,sleep是释放cpu抢占资源,而wait是释放锁的意思。线程wait释放锁后,就一直被阻塞,苏醒后继续抢到锁,然后执行未完成代码。
wait苏醒后从被wait地方继续执行代码,而不是从函数或者synchronized开始执行。
如下代码模拟生产者与消费者,由于生产者的方法入口添加sleep,所以消费者一定先抢到了锁,输出 begin decrement,来到锁里面输出begin decrement1,但是由于队列空,只能wait,把锁给释放了。此时生产者拿到锁,输出了begin increment和begin increment1,给队列生产1个数据,通知消费者消费。注意消费者苏醒后直接从lock.wait()这行代码后继续执行,继续while循环判断。
arduino
begin decrement
begin decrement1
begin increment
begin increment1
Java
public class Main {
private static int max = 1;
private static Object lock = new Object();
private static List<Integer> list = new ArrayList<>();
public static void increment() {
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("begin increment");
synchronized (lock) {
System.out.println("begin increment1");
while (list.size() >= max) {
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
list.add(1);
lock.notifyAll();
}
}
public static void decrement() {
System.out.println("begin decrement");
synchronized (lock) {
System.out.println("begin decrement1");
while (list.size() == 0) {
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
list.remove(list.size() - 1);
lock.notifyAll();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
increment();
});
Thread t2 = new Thread(()->{
decrement();
});
t1.start();
t2.start();
}
}
为什么边界条件都是while,而不是if?
if会造成条件失效,模拟下如果3个生产者,1个消费者,队列最大是1。
markdown
1. 生产者t1,生产1个,通知其它线程抢锁
2. 生产者t2,发现队列满了,把自己wait,
3. 生产者t3,发现队列还是满了,把自己wait,
4. 消费者进来,消费1个,通知其它线程抢锁
5. t2苏醒,直接走if后面的代码,队列生产1个
6. t3苏醒,直接走if后面的代码,队列生产2个,报错。
3.3 notify
notify是wait对立面,它手上有锁,告诉被wait的线程,你可以去外面排队了,我马上就释放锁了,注意此时还没有释放锁,只有synchronized代码块执行结束才释放锁。而被wait的线程收到notify通知后,不睡觉了,在门口等待抢锁动作。
| notify() | notifyAll() | |
|---|---|---|
| 叫醒几个 | 一个(哪个不确定) | 这把锁上 所有 wait 的 |
| 风险 | 可能叫错人:叫醒了另一个生产者,消费者还在睡 | 大家醒了再用 while 判断,不会干错 |
| 入门 | 少用 | 优先用这个 |
四、死锁
死锁必须满足
- 互斥: 锁同时只能一人拿
- 持有并等待: t1拿着 lock1 不放,再等 lock2
- 不可剥夺: 不能把别人的锁抢走,只能等他自己放
- 循环等待: t1 等 t2,t2 等 t1,围成环
Java
public class Main {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized(lock1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(lock2){
System.out.println("Thread 1 locked lock2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized(lock2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(lock1){
System.out.println("Thread 2 locked lock1");
}
}
});
t1.start();
t2.start();
}
}