出自:已或授权https://blog.csdn.net/sky_dsy/article/details/78912461
一. 多线程的实现
1.1 实现多线程的三种方式
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
实现 Runnable 接口
需要实现 run() 方法。
通过 Thread 调用 start() 方法来启动线程。
public class MyRunnable implements Runnable {
public void run() {
// ...
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Tread thread = new Thread(instance);
thread.start();
}
}
实现 Callable 接口
与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
public class MyCallable implements Callable{
public Integer call() {
// ...
}
public static void main(String[] args) {
MyCallable mc = new MyCallable();
FutureTaskft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
}
继承 Thread 类
同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程。
public class MyThread extends Thread {
public void run() {
// ...
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
}
实现接口 VS 继承 Thread
实现接口会更好一些,因为:
1.2 Thread和Runable的区别和联系
(1)联系:
(2)不同:
二.基础线程机制
sleep()
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。也可以使用 TimeUnit.TILLISECONDS.sleep(millisec)。
sleep() 可能会抛出 InterruptedException。因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
public void run() {
try {
// ...
Thread.sleep(1000);
// ...
} catch (InterruptedException e) {
System.err.println(e);
}
}
yield()
对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。
public void run() {
// ...
Thread.yield();
}
join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,直到目标线程结束。
可以加一个超时参数。
deamon
守护线程(deamon)是程序运行时在后台提供服务的线程,并不属于程序中不可或缺的部分。
当所有非后台线程结束时,程序也就终止,同时会杀死所有后台线程。
main() 属于非后台线程。
使用 setDaemon() 方法将一个线程设置为后台线程。
三.线程的状态转换
线程的阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
四.结束线程
4.1阻塞
一个线程进入阻塞状态可能有以下原因:
4.2中断
使用中断机制即可终止阻塞的线程。
使用 interrupt() 方法来中断某个线程,它会设置线程的中断状态。Object.wait(), Thread.join() 和 Thread.sleep() 三种方法在收到中断请求的时候会清除中断状态,并抛出 InterruptedException。
应当捕获这个 InterruptedException 异常,从而做一些清理资源的操作。
1. 不可中断的阻塞
不能中断 I/O 阻塞和 synchronized 锁阻塞。
2. Executor 的中断操作
Executor 避免对 Thread 对象的直接操作,但是使用 interrupt() 方法必须持有 Thread 对象。Executor 使用 shutdownNow() 方法来中断它里面的所有线程,shutdownNow() 方法会发送 interrupt() 调用给所有线程。
如果只想中断一个线程,那么使用 Executor 的 submit() 而不是 executor() 来启动线程,就可以持有线程的上下文。submit() 将返回一个泛型 Futrue,可以在它之上调用 cancel(),如果将 true 传递给 cancel(),那么它将会发送 interrupt() 调用给特定的线程。
3. 检查中断
通过中断的方法来终止线程,需要线程进入阻塞状态才能终止。如果编写的 run() 方法循环条件为 true,但是该线程不发生阻塞,那么线程就永远无法终止。
interrupt() 方法会设置中断状态,可以通过 interrupted() 方法来检查中断状,从而判断一个线程是否已经被中断。
interrupted() 方法在检查完中断状态之后会清除中断状态,这样做是为了确保一次中断操作只会产生一次影响。
4 如何中断线程的执行?
(1)设置退出标志(boolean类型的变量),使线程正常退出,也就是当run()方法完成后线程终止。
(2)使用 interrupt() 方法中断线程。
(3)使用 stop 方法强行终止线程(不推荐使用,Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 这些终止线程运行的方法已经被废弃,使用它们是极端不安全的!)
如果强制让线程停止有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了解锁,导致数据得不到同步的处理,可能出现数据不一致的问题。
五、线程之间的协作
5.1同步与通信的概念理解
在操作系统中,有三个概念用来描述进程间的协作关系:
通信是一种手段,它可以用来实现同步。也就是说,通过在多个进程间传递信息,可以控制多个进程以一定顺序执行。
而同步又可以保证互斥。即进程按一定顺序执行,可以保证在同一时刻只有一个进程能访问临界资源。但是同步不止用来实现互斥,例如生成者消费者问题,生产者和消费者进程之间的同步不是用来控制对临界资源的访问。
总结起来就是:通信 --> 同步 --> 互斥。
进程和线程在一定程度上类似,也可以用这些概念来描述。
在 Java 语言中,这些概念描述有些差别:
5.2线程同步
给定一个进程内的所有线程,都共享同一存储空间,这样有好处又有坏处。这些线程就可以共享数据,非常有用。不过,在两个线程同时修改某一资源时,这也会造成一些问题。Java 提供了同步机制,以控制对共享资源的互斥访问。
5.2.1. synchronized
同步一个方法
使多个线程不能同时访问该方法。
public synchronized void func(String name) {
// ...
}
同步一个代码块
public void func(String name) {
synchronized(this) {
// ...
}
}
synchronized关键字
5.2.2. Lock
实现更细粒度的控制。
private Lock lock;
public int func(int value) {
try {
lock.lock();
// ...
} finally {
lock.unlock();
}
}
5.2.3 synchronized与Lock的区别
lock是一个类,主要有以下几个方法:
lock():获取锁,如果锁被暂用则一直等待
unlock():释放锁
tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true
tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间
(1)Lock的加锁和解锁都是由java代码实现的,而synchronize的加锁和解锁的过程是由JVM管理的。
(2)synchronized能锁住类、方法和代码块,而Lock是块范围内的。
(3)Lock能提高多个线程读操作的效率;
(4)Lock:Lock实现和synchronized不一样,后者是一种悲观锁,它胆子很小,它很怕有人和它抢吃的,所以它每次吃东西前都把自己关起来。而Lock底层其实是CAS乐观锁的体现,它无所谓,别人抢了它吃的,它重新去拿吃的就好啦,所以它很乐观。如果面试问起,你就说底层主要靠volatile和CAS操作实现的。
5.3线程通信
5.3.1. wait() notify() notifyAll()
它们都属于 Object 的一部分,而不属于 Thread。
wait() 会在等待时将线程挂起,而不是忙等待,并且只有在 notify() 或者 notifyAll() 到达时才唤醒。
sleep() 和 yield() 并没有释放锁,但是 wait() 会释放锁。
实际上,只有在同步控制方法或同步控制块里才能调用 wait() 、notify() 和 notifyAll()。
private boolean flag = false;
public synchronized void after() {
while(flag == false) {
wait();
// ...
}
}
public synchronized void before() {
flag = true;
notifyAll();
}
wait() 和 sleep() 的区别
1.wait/notify机制
5.3.2.同步
(多个线程通过synchronized关键字这种方式来实现线程间的通信)
本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。
5.3.3.while轮询的方式
尽管线程A一直在while中执行,需要占用CPU。但是,线程的调度是由JVM或者说是操作系统来负责的,并不是说线程A一直在while循环,然后线程B就占用不到CPU了。对于线程A而言,它就相当于一个“计算密集型”作业了。如果我们的while循环是不断地测试某个条件是否成立,那么这种方式就很浪费CPU。如果同步快中代码进入了死循环,则可能导致多线程无法继续执行下去。
5.3.4.管道通信
(通过管道,将一个线程中的消息发送给另一个)
5.3.5. BlockingQueue
java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将一直阻塞到队列中有内容,如果队列为满 put() 将阻塞到队列有空闲位置。它们响应中断,当收到中断请求的时候会抛出 InterruptedException,从而提前结束阻塞状态。
阻塞队列的 take() 和 put() 方法是线程安全的。
使用 BlockingQueue 实现生产者消费者问题
// 生产者
import java.util.concurrent.BlockingQueue;
public class Producer implements Runnable {
private BlockingQueuequeue;
public Producer(BlockingQueuequeue) {
this.queue = queue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is making product...");
String product = "made by " + Thread.currentThread().getName();
try {
queue.put(product);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 消费者
import java.util.concurrent.BlockingQueue;
public class Consumer implements Runnable {
private BlockingQueuequeue;
public Consumer(BlockingQueuequeue) {
this.queue = queue;
}
@Override
public void run() {
try {
String product = queue.take();
System.out.println(Thread.currentThread().getName() + " is consuming product " + product + "...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 客户端
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Client {
public static void main(String[] args) {
BlockingQueuequeue = new LinkedBlockingQueue<>(5);
for (int i = 0; i < 2; i++) {
new Thread(new Consumer(queue), "Consumer" + i).start();
}
for (int i = 0; i < 5; i++) {
// 只有两个 Product,因此只能消费两个,其它三个消费者被阻塞
new Thread(new Producer(queue), "Producer" + i).start();
}
for (int i = 2; i < 5; i++) {
new Thread(new Consumer(queue), "Consumer" + i).start();
}
}
}
// 运行结果
Producer0 is making product...
Consumer0 is consuming product made by Consumer0...
Producer1 is making product...
Consumer1 is consuming product made by Consumer1...
Producer2 is making product...
Producer3 is making product...
Producer4 is making product...
Consumer2 is consuming product made by Consumer2...
Consumer3 is consuming product made by Consumer3...
Consumer4 is consuming product made by Consumer4...