多线程实现方式
Thread类
MyThread类继承了Thread类
MyThread thread= new MyThread1("窗口1");
thread.start();
Runnable接口
自定义一个MyRunnable类来实现Runnable接口,在MyRunnable类中重写run()方法
,创建Thread对象,并把MyRunnable对象作为Tread类构造方法的参数传递进去。
MyRunnable myrunnable = new MyRunnable();
Thread thread = new Thread(myrunnable, "线程01");
thread.start();
lambda转换为Runnable功能接口(interface)的子类的实例,然后将其传递给overloaded to take a Runnable object的Thread构造函数。
new Thread(() -> System.out.println("Lol")).start();
Callable接口
自定义一个MyThread类来实现Callable接口,在MyThread类中重写call()方法,创建FutureTask,Thread对象,并把MyCallable对象作为FutureTask类构造方法的参数传递进去,把FutureTask对象传递给Thread对象。可以获取到线程的执行结果。
public class MyThread implements Callable {
@Override
public Object call() throws Exception {
return 100;
}
}
MyThread myThread=new MyThread();
FutureTask futureTask=new FutureTask(myThread);
Thread thread=new Thread(futureTask);
thread.start();
try {
Object o = futureTask.get();
System.out.println(o);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
方法介绍
![](https://i-blog.csdnimg.cn/blog_migrate/4131e66da4d29bf7b41478ff1218984e.png)
线程生命周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过 新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直"霸占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是 线程状态也会多次在运行、阻塞之间切换。
新建状态(NEW):线程已创建,尚未调用start()方法启动之前。
运行状态(RUNNABLE):线程对象被创建后,调用该对象的start()方法,并获取CPU权限进行执行。
阻塞状态(BLOCKED):线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
等待状态(WAITING ):等待状态。正在等待另一个线程执行特定动作来唤醒该线程的状态。
超时等待状态(TIME_WAITING):有明确结束时间的等待状态。
终止状态(TERMINATED ):当线程结束完成之后就会变成此状态。
线程的操作方法
start:使该线程开始执行,Java虚拟机底层调用该线程的start0( )方法;
run:调用线程对象run方法。start底层会创建新的线程,run是一个简单的方法调用,不会启动新线程。
sleep:在指定的毫秒数内让当前正在执行的线程休眠;醒来后进入就绪状态。
interrupt:中断线程,但并没有真正结束线程,所以一般用于中断正在休眠线程。
yield:线程的礼让。yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
join:线程的插队
守护线程
线程分为用户线程和守护线程。虚拟机必须确保用户线程执行完毕。虚拟机在非守护线程都结束时,守护线程会陆续结束。
线程同步
线程安全
什么是线程安全性
当多个线程访问某个类时,不管运行时环境采用 何种调度方式 或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类就是线程安全的。
线程安全性的三个体现
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作(Atomic、CAS算法、synchronized、Lock)
可见性:一个主内存的线程如果进行了修改,可以及时被其他线程观察到(synchronized、volatile)
有序性:如果两个线程不能从 happens-before原则 观察出来,那么就不能观察他们的有序性,虚拟机可以随意的对他们进行重排序,导致其观察观察结果杂乱无序(happens-before原则)
线程同步机制
解决线程并发问题的方法是线程同步,线程同步就是让线程排队,就是操作共享资源要有先后顺序,一个线程操作完之后,另一个线程才能操作或者读取。
使用synchronized关键字, 同步方法或者同步代码块。需要唯一的同步监视器。
买票问题
public class MyThread1 extends Thread{
public MyThread1(String name) {
super(name);
}
static int ticket=0;
static Lock lock=new ReentrantLock();
@Override
public void run() {
while (true){
lock.lock();
if(ticket==100)
break;
else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(getName()+"在卖第"+ticket+"张票");
lock.unlock();
}
}
}
}
MyThread1 thread= new MyThread1("窗口1");
MyThread1 thread1=new MyThread1("窗口2");
MyThread1 thread2=new MyThread1("窗口3");
thread.start();
thread1.start();
thread2.start();
注:
Volatile关键字的作用主要有如下两个:
1.线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
2. 顺序一致性:禁止指令重排序。
死锁
多个线程各自占有一个资源,并且相互等待其他线程占有的资源才能运行,从而导致另个或者多个线程都在等待对方释放资源,都停止了执行。某一个同步代码块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
Lock 锁也称同步锁,java.util.concurrent.locks.Lock 机制提供了⽐ synchronized 代码块和 synchronized ⽅法更⼴泛的锁定操作,同步代码块 / 同步⽅法具有的功能 Lock 都有,除此之外更强⼤,更体现⾯向对象。创建对象 Lock lock = new ReentrantLock() ,加锁与释放锁⽅法如下:
public void lock() :加同步锁
public void unlock() :释放同步锁
synchronized和Lock的对比:
Lock是显式锁(手动开启和关闭锁,别忘记关闭),synchronized是隐式锁,除了作用域就自动释放。
Lock只是代码块锁(执行体放在开启锁和关闭锁中间),synchronized有代码块锁和方法锁。
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
优先使用顺序:Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)。
线程池
经常创建和销毁线程,消耗特别大的资源,比如并发的情况下的线程,对性能影响很大。线程池就是问题为了解决这个问题,提前创建好多个线程,放在线程池中,使用时直接获取,使用完放回线程池中,可以避免频繁的创建、销毁,实现重复利用。
ExecutorService executorService = Executors.newCachedThreadPool();
MyRunnable myThread2=new MyRunnable();
executorService.submit(myThread2);
阻塞队列
他也是队列的一种,那么他肯定是一个先进先出(FIFO)的数据结构。与普通队列不同的是,他支持两个附加操作,即阻塞添加和阻塞删除方法。BlockingQueue是实现了Queue接口,一般使用两种如下。
-
ArrayBlockingQueue 由数组构成的有界阻塞队列
-
LinkedBlockingQueue 由链表构成的有界阻塞队列
阻塞添加:当阻塞队列是满时,往队列里添加元素的操作将被阻塞。
阻塞移除:当阻塞队列是空时,从队列中获取元素/删除元素的操作将被阻塞。
生产
add、offer、put这3个方法都是往队列尾部添加元素,区别如下:
add:不会阻塞,添加成功时返回true,不响应中断,当队列已满导致添加失败时抛出IllegalStateException。
offer:不会阻塞,添加成功时返回true,因队列已满导致添加失败时返回false,不响应中断。
put:会阻塞会响应中断。
消费
take、poll方法能获取队列头部第1个元素,区别如下:
take:会响应中断,会一直阻塞直到取得元素或当前线程中断。
poll:会响应中断,会阻塞,阻塞时间参照方法里参数timeout.timeUnit,当阻塞时间到了还没取得元素会返回null
综合练习(leetcode)
给你一个类:
public class Foo {
public void first() { print("first"); }
public void second() { print("second"); }
public void third() { print("third"); }
}
三个不同的线程 A、B、C 将会共用一个 Foo 实例。
线程 A 将会调用 first() 方法
线程 B 将会调用 second() 方法
线程 C 将会调用 third() 方法
请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。
输入:nums = [1,2,3]
输出:"firstsecondthird"
解释:
有三个线程会被异步启动。输入 [1,2,3] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 second() 方法,线程 C 将会调用 third() 方法。正确的输出是 "firstsecondthird"。
引用leetcode精选代码
public class Foo {
private volatile int flag = 1;
//创建一把锁
private final Object object = new Object();
public Foo() {
}
public void first(Runnable printFirst) throws InterruptedException {
synchronized (object) {
while (flag != 1){
//如果当前标志不是1,那就阻塞当前线程,释放锁,等待被唤醒再重新执行
//被唤醒的线程需要重新竞争锁对象,获得锁的线程可以从wait处继续往下执行
object.wait();
}
printFirst.run();
flag = 2;
object.notifyAll();
}
}
public void second(Runnable printSecond) throws InterruptedException {
synchronized (object) {
while (flag != 2){
//如果当前标志不是2,那就阻塞当前线程,释放锁,等待被唤醒再重新执行
//被唤醒的线程需要重新竞争锁对象,获得锁的线程可以从wait处继续往下执行
object.wait();
}
printSecond.run();
flag = 3;
object.notifyAll();
}
}
public void third(Runnable printThird) throws InterruptedException {
synchronized (object) {
while (flag != 3){
//如果当前标志不是3,那就阻塞当前线程,释放锁,等待被唤醒再重新执行
//被唤醒的线程需要重新竞争锁对象,获得锁的线程可以从wait处继续往下执行
object.wait();
}
}
printThird.run();
}
}