多线程
Java开启子线程的方式
继承Thread类
public class MyThread extends Thread{
public void run() {
super.run();
System.out.println(Thread.currentThread().getName()+"Thread方法实现多线程");
}
MyThread thread = new MyThread();
thread.start();
实现Runable接口(避免单继承的局限性)
public class MyThread implements Runnable{
public void run() {
System.out.println(Thread.currentThread().getName()+"Thread方法实现多线程");
}
}
Thread thread = new Thread(new MyThread());
thread.start();
实现Callable
public class MyCallable implements Callable
private int count = 20;
@Override
public String call() throws Exception {
for (int i = count; i > 0; i--) {
System.out.println(Thread.currentThread().getName()+"当前票数:" + i);
}
return "sale out";
}
Callable
// FutureTask是RunableFuture的子类
FutureTask
Thread thread = new Thread(ft);
thread.start();
String s = ft.get(); // 获取call的返回值
注意:Thread才是真正的线程类,Runable和Callable是对任务task的封装
Callable:
同Runable接口,这个接口描述的任务有返回值
public interface Callable
V call() throws Exception; // 类似Runable的run方法(run方法没有返回值)
}
Future:
是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
get():等待线程执行完毕,返回线程的结果
cancel():停止线程
isDown():判断线程是否结束
isCancel():判断线程是否取消
FutureTask:
实现了RunableFuture接口(RunableFuture继承了Runable,Future接口),这个类既可以作为Runable任务被线程类Thread执行,又可以当作Future拿到Callable任务的返回值;
线程的终止
suspend():暂停
resume():继续
stop():终止
上面三个方法都已经过时了,这些方法的调用线程不会释放持有的资源,容易引发死锁
协作式关闭线程
interrupt():调用这个方法会将线程的中断标志设置为true,表示告诉这个线程你该中断了,但是当前线程并一定会理会这个中断操作
isInterrupted():用于判断当前的线程是否执行过interrupt(),即判断中断标志位是否为true;
interrupted():这是一个static方法,也用来判断中断标志位,不过会多一步操作,将标志位设置为false;
生命周期&线程方法
start()&run():
start():
public synchronized void start() {
if (this.threadStatus != 0) {
throw new IllegalThreadStateException();
} else {
this.group.add(this);
boolean started = false;
try {
this.start0(); // 调用native方法开启子线程
started = true;
} finally {
try {
if (!started) {
this.group.threadStartFailed(this);
}
} catch (Throwable var8) {
}
}
}
}
private native void start0();
run():
private Runnable target;
public void run() {
if (this.target != null) {
this.target.run(); // 调用Runable(任务)的run方法(线程的逻辑代码)
}
}
run是线程类的逻辑方法,start是开启线程的一个native方法
sleep():进入阻塞状态
yield():让出当前线程的cpu调度,使cpu重新调度(可能还会调度到当前线程)
join():插队,让插入的线程执行完再继续执行当前线程(可以迭代插入,B插入A,C可以插入B,D可以插入C)
wait()/notify()/notifyAll() :下文重点讲解
线程共享与协作
synchronized关键字:
加锁,确保多个线程在同一时刻只能由一个线程处于这个方法或代码块中;
对象锁:用于对象实例方法或者对象实例上
synchronized方法
// 共享资源
static int ticketNum = 0;
private synchronized void getTicketSync() {
ticketNum--;
System.out.println(Thread.currentThread().getName() + " 余票: " + ticketNum);
}
这个方法加了synchronized关键字后,对ticketNum的--操作同一时刻只能由一个线程执行;
synchronized代码块
private void getTicketSync() {
synchronized (this) {
ticketNum--;
System.out.println(Thread.currentThread().getName() + " 余票: " + ticketNum);
}
}
类锁:用于static方法(锁:类的class对象)
private static synchronized void getTicketSync() {
ticketNum--;
System.out.println(Thread.currentThread().getName() + " 余票: " + ticketNum);
}
等待通知机制:
是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作;
wait():调用该方法的进程进入阻塞状态,并且该进程释放持有的锁
wait(long):经过一段时间后如果没有唤醒就超时返回
notify():通知一个在该对象上等待的线程,执行wait()下面的代码,要求是该线程必须获得锁;如果没有获得锁就继续阻塞;
notifyAll():通知这个对象上的所有线程
标准范式:
等待方:
获取对象锁
循环中判断是否满足条件,不满足调用wait方法(调用wait会释放锁),满足就执行具体业务代码
等待方:
获取对象锁
修改判断条件,通知等待方(notify/notifyAll)
创建一个快递类
public class Express {
private int km;
public Express(int km) {
this.km = km;
}
public synchronized void changeKm(){
// 获取锁
System.out.println("通知线程:----获取对象锁");
// 改变条件,通知
System.out.println("通知线程:----改变距离超过100,通知等待线程");
this.km = 101;
notifyAll();
// 释放锁
System.out.println("通知线程:----释放对象锁");
}
public synchronized void waitKm(){
// 获取锁
System.out.println("等待线程:----获取对象锁");
// 循环,不满足条件调用wait方法
while (this.km<100){
try {
System.out.println("等待线程:----此时距离小于100,当前线程调用wait等待,释放对象锁");
wait();
System.out.println("等待线程:----当前线程从wait返回并且获得锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 满足条件,执行具体业务逻辑
System.out.println("等待线程:----距离大于100(等待方等待到通知信号后),执行具体的业务逻辑");
// 释放锁
System.out.println("等待线程:----释放对象锁");
}
}
创建两个线程,wait线程,notify线程
public class Main {
static Express express = new Express(0);
static class WaitThread extends Thread{
@Override
public void run() {
express.waitKm();
}
}
static class NotifyThread extends Thread{
@Override
public void run() {
express.changeKm();
}
}
public static void main(String[] args) {
new WaitThread().start();
new NotifyThread().start();
}
}
先执行wait线程,在执行notiify线程,运行结果:
首先等待线程先获得锁,循环判断当前的距离是否小于100,如果小于100就wait,释放锁,然后通知线程获取对象锁,改变距离超过100,notify通知,然后释放对象锁,等待线程收到notify通知并且获得对象锁从wait方法返回,执行具体的业务逻辑,最后释放锁;
如果是先执行notify线程,再执行wait线程:
public static void main(String[] args) {
new NotifyThread().start();
new WaitThread().start();
}
通知线程获得锁,改变距离超过100,释放锁;等待线程获得锁,判断直接满足条件距离大于100,不需要wait操作,直接执行业务代码;
ThreadLocal:
ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
ThreadLocal往往用来实现变量在线程之间的隔离
创建一个ThreadLocal对象,指定存储的数据为integer,初始值为100
static ThreadLocal
@Override
protected Integer initialValue() {
return 100;
}
};
创建一个add线程(加法)和sub线程(减法)
static class ADDThread extends Thread{
@Override
public void run() {
Integer integer = threadLocal.get();
System.out.println(Thread.currentThread().getName()+"----初始值----"+integer);
integer +=100;
System.out.println(Thread.currentThread().getName()+"----线程 操作int+100 ----");
threadLocal.set(integer);
System.out.println(Thread.currentThread().getName()+"----调用set存放新的integer到threadlocal,保存修改");
System.out.println(Thread.currentThread().getName()+"----当前线程的副本值"+threadLocal.get());
new SubThread().start();
}
}
static class SubThread extends Thread{
@Override
public void run() {
Integer integer = threadLocal.get();
System.out.println(Thread.currentThread().getName()+"----初始值----"+integer);
}
}
启动addThread
public static void main(String[] args) {
new ADDThread().start();
}
我们在add线程中操作了这个int,将他加了100,在add线程中打印出来时200,启动sub线程打印threadlocal中存放的这个值发现还是100;
ThreadLocal原理:
ThreadLocal内部有一个内部类ThreadLocalMap;ThreadLocalMap的内部有一个Entry内部类,ThreadLocalMap内部类维护了一个Entry数组,Entry(类似Map.Entry)key是ThreadLocal,value是Object;每一个线程都有一个ThreadLocalMap的实例,这个ThreadLocalMap内部又有一个Entry数组,将threadLocal作为key获取每个线程中独立的副本,因为threadLocal可以有多个,所以Entry以数组的形式存放;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
显式锁 Lock
Lock接口:
lock接口的方法
lock(): 获取锁
lockInterruptibly():可中断的获取锁
unlock(): 释放锁
tryLock(): 尝试获取锁
newCondition(): 用于lock的通知等待机制,下面会详细介绍
显示锁的范式
先获取锁lock.lock();
处理业务后,必须在finally中lock.unlock();
必须在finally中释放锁,因为如果在业务代码中抛出异常,这个锁就永远无法释放了
Lock lock = new ReentrantLock(); // Lock接口的实现类(可重入锁)
public void lockTest(){
lock.lock(); // 获取锁
try {
// 业务代码
System.out.println("拿到锁后,执行相应的业务代码");
}finally {
lock.unlock(); // 释放锁,必须在finally中释放锁
}
}
可重入锁:
ReentrantLock是一个可重入锁,执行线程在调用lock.lock()获取了这个锁以后,如果这个方法是一个递归方法,会继续调用lock.lock(),这时可重入锁就可以再次被这个线程获得,即同一线程可以多次获得这个锁,在synchronized内部jdk也加入了可重入机制
公平/非公平锁:
公平锁:等待时间越长的线程先获得锁
非公平锁: 后创建的线程先获得锁(等待时间短的先获得锁)
public ReentrantLock(boolean fair) {
this.sync = (ReentrantLock.Sync)(fair ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
}
ReentrantLock提供了一个构造方法,创建的锁是否公平;
非公平锁为什么比公平锁效率高?
因为线程在切换的时候会设计到操作系统的上下文切换,这是一个消耗资源的操作,现在有AB两个线程,A获取了锁,B正在等待(处于阻塞状态),如果这时A释放了锁,创建了一个新线程C,根据非公平锁的概念,这个锁应该给C线程,C线程省去了上下文切换的步骤,所以非公平锁的效率比公平锁高;
读写锁:
ReadWriteLock接口
当读写锁被读线程持有时,其他的读线程可以共享这个资源,写线程不可以;当这个锁被写线程持有时,其他所有的读线程,写线程都不可以共享;
读写锁的使用
ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // ReadWriteLock的实现类,可重入读写锁
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
public void read(){
readLock.lock();
try {
// 业务代码
}finally {
readLock.unlock();
}
}
public void write(){
writeLock.lock();
try {
// 业务代码
}finally {
writeLock.unlock();
}
}
Condition实现显示锁Lock的等待通知机制:
将上面等待通知的例子做如下改写,其他代码都不变
public class Express {
private int km;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public Express(int km) {
this.km = km;
}
public void changeKm(){
// 获取锁
System.out.println("通知线程:----获取对象锁");
lock.lock();
// 改变条件,通知
try {
System.out.println("通知线程:----改变距离超过100,通知等待线程");
this.km = 101;
condition.signalAll();
}finally {
// 释放锁
System.out.println("通知线程:----释放对象锁");
lock.unlock();
}
}
public void waitKm(){
// 获取锁
lock.lock();
System.out.println("等待线程:----获取对象锁");
// 循环,不满足条件调用wait方法
try {
while (this.km<100){
try {
System.out.println("等待线程:----此时距离小于100,当前线程调用wait等待,释放对象锁");
condition.await();
System.out.println("等待线程:----当前线程从wait返回并且获得锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 满足条件,执行具体业务逻辑
System.out.println("等待线程:----距离大于100(等待方等待到通知信号后),执行具体的业务逻辑");
}finally {
// 释放锁
System.out.println("等待线程:----释放对象锁");
lock.unlock();
}
}
}
先执行等待线程
先执行通知线程
效果和上面的是一样的,分析参照上面的分析;