线程
线程是什么
线程的创建方式
线程的生命周期
线程的常用方法
同步锁
死锁
通知和等待
线程池
线程是什么
线程是最小的执行单元;程序,进程,线程;一个程序可以包含多个进程,一个进程可以包含多个线程。在java中可以让线程独立的执行一件事,而和其他的线程互不影响。线程简单理解就是可以同时去做多件事情。
在任务管理器中可以按到进程,如果想要看到线程的信息就需要通过工具类查看(jdk/bin/jconsole),如果要看更加详细的信息,或者针对程序做性能调整可以使用工具JProfiler
线程的创建方式
线程的创建方式有三种:
继承Thread类
package com.zlt.threads;
public class MyThread extends Thread{
@Override
public void run() {
super.run();
//线程的功能在这个run方法中进行实现
for (int i = 0; i < 100; i++) {
System.out.println("线程输出" + i);
}
}
public static void main(String[] args) {
//创建线程对象
MyThread myThread = new MyThread();
myThread.start();//启动线程
for (int i = 0; i < 100; i++) {
System.out.println("主线程输出" + i);
}
}
}
继承Thread类实现线程是最简单的一种实现方案,但是因为java是单继承,所以继承了Thread以后就无法再继承其他的类了,扩展性更差一些
实现Runnable接口
package com.zlt.threads;
public class MyRunnable implements Runnable{
@Override
public void run() {
//线程实现的任务,在这个run方法中
for (int i = 0; i < 100; i++) {
System.out.println("runable线程输出" + i);
}
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
//启动线程依赖于Thread类
MyThread thread = new MyThread(myRunnable);
thread.start();//启动线程
/*for (int i = 0; i < 100; i++) {
System.out.println("主线程输出" + i);
}*/
}
}
创建一个类实现了Runnable接口,重写了接口中的run方法,创建对象后依赖于Thread去创建线程并执行run方法,需要注意的是,Thread的start执行后回去创建线程执行Thread的run方法,而Thread的run方法调用了Runnable中的run方法。这样Runnable中的run就可以在线程中执行起来了。因为是实现接口,扩展性更强。
实现Callable接口
package com.zlt.threads;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
//线程执行的任务
for (int i = 0; i < 100; i++) {
System.out.println("线程输出" + i);
}
return 100;
}
public static void main(String[] args) {
MyCallable callable = new MyCallable();
//FutureTask 实现了Runnable接口,里面有一个run方法,run方法调用了callable的call方法
FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
//获取返回值只能在启动线程以后才可以获取
Integer integer = futureTask.get();
System.out.println(integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
FutureTask 实现了Runnable接口,里面有一个run方法,run方法调用了callable的call方法,call方法执行完毕获取到返回值,将返回值保存下来,可以通过get方法获取到call方法的返回值
创建线程如何选择哪一种
如果只是单纯的创建一个线程去执行任务,而不需要考虑其他的事情,可以使用继承Thread类
如果实现线程需要考虑扩展性,建议使用Runnable
如果需要知道线程执行完毕后的情况,建议使用Callable,只有Callable是有返回值的
线程的生命周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DbQ7QrPj-1681732512728)(线程.assets/图片1.png)]
一般说生命周期就是说从创建开始到死亡,到底经历了一些什么事。
当创建了线程对象,线程处于新建状态,然后调用了start方法,线程处于就绪状态。就绪状态的线程会进入线程队列进行排队,当获取到cpu资源后,线程处于执行状态。在执行状态的线程,可能因为一些原因(IO阻塞,线程休眠等)进入阻塞状态,当阻塞原因消失后,线程会再次进入队列,等到cpu的资源,从上一次的中断处继续执行。当线程正常的执行完run方法则会进入死亡状态。
新建: 线程对象状态
就绪:线程进入线程队列,等待cpu的资源
执行:正在执行run方法
阻塞:主动释放cpu资源,等待阻塞原因消失
死亡:线程的执行结束,进入死亡状态
线程的常用方法
package com.zlt.threads;
public class MyRunnable implements Runnable{
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程实现的任务,在这个run方法中
for (int i = 0; i < 100; i++) {
System.out.println("runable线程输出" + i);
}
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
//启动线程依赖于Thread类
Thread thread = new Thread(myRunnable,"线程A");//给线程取一个名字
thread.getName();//获取线程的名字
thread.setName("线程");//给线程设置一个名字
// 线程中有三个优先级的常量 分别是 1 5 10 在设置线程优先级的时候建议使用常量设置 默认的优先级为5
int priority = thread.getPriority();//获取当前线程的优先级
// thread.setPriority(newPriority);//设置线程的优先级
//设置线程的优先级 取值范围是1-10
// Thread.MAX_PRIORITY 10
// Thread.NORM_PRIORITY 5 默认的优先级
// Thread.MIN_PRIORITY 1
//设置是否为守护线程
/*
守护线程也叫做后台线程,比较典型的守护线程就是垃圾回收线程。
主线程执行完毕后,还剩下其他的线程没有执行完毕则会等待其他的线程执行完毕,虚拟机才会结束
但是主线程执行完毕后,只剩下守护线程还没有执行完毕,虚拟机会直接结束,而不会等待守护线程执行完毕
true 表示是守护线程
false表示非守护线程 默认是false
*/
// thread.setDaemon(on);
thread.isDaemon();//判断线程是否为守护线程
//线程是否还活着 刚创建还没有调用start方法时 为false 调用了start后为true run方法执行完毕后为false
boolean alive = thread.isAlive();
System.out.println(alive);
// 调用了start0 而这个方法是一个本地方法,调用了其他的语言去启动线程,线程启动后再执行run方法
thread.start();//启动线程
// thread.stop();//停止线程已经过时,不建议使用,防止线程中的任务没有执行完毕 防止业务出现问题
alive = thread.isAlive();
System.out.println(alive);
try {
//让线程休眠 只有一个参数的方法,单位为毫秒
Thread.sleep(1000);
//让线程休眠,一个为毫秒,一个为纳秒
Thread.sleep(1000,200);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.yield();//让占用cpu资源的线程主动让出cpu的资源,处于就绪状态
//中断线程 唤醒线程 如果线程处于休眠状态 可以唤醒休眠马上去执行任务 但是会抛出一个异常InterruptedException
// thread.interrupt();
//是否被中断
// thread.isInterrupted()
//获取当前占用cpu资源的线程对象 其实就是当前线程对象
Thread currentThread = Thread.currentThread();
alive = currentThread.isAlive();
System.out.println(alive);
}
}
锁
同步锁
当多个线程同时操作一个资源的时候,资源会出现紊乱的情况。这个时候希望同时只能有一个线程去操作,这个时候其他的线程不能操作,只能在旁边等着
可以使用同步锁来进行处理
同步锁是一个独占锁,一个线程加了锁以后,其他的线程就需要等待,而不能去进行处理。
同步方法
package com.zlt.threads;
public class Ticket implements Runnable{
private int num = 10;//表示有十张票
/**
* 在方法上添加了关键字synchronized 那么当 第一个线程调用这个方法的时候就会给这个方法添加一个锁,在执行完毕之前,
* 这个锁一直存在。其他的线程要想调用这个方法,只能等锁被释放以后才可以调用。当第一个线程执行完方法,释放锁以后,
* 所有要调用这个方法的线程都可以一起去抢夺调用权,谁抢到了谁又加锁,没有抢到的又继续等
* @return
*/
public synchronized boolean sell() {
if(num > 0) {
String name = Thread.currentThread().getName();
System.out.println(name + "购买了第" + num + "张票");
num--;
return true;
}else {
return false;
}
}
@Override
public void run() {
while(true) {
if(!sell()) {
break;
}
}
}
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket, "a");
Thread thread2 = new Thread(ticket, "b");
Thread thread3 = new Thread(ticket, "c");
thread1.start();
thread2.start();
thread3.start();
}
}
同步代码块
package com.zlt.threads;
public class Ticket implements Runnable{
private int num = 10;//表示有十张票
private Object lock = new Object();
@Override
public void run() {
while(true) {
//锁需要加在指定的对象上面,要确保多线程访问的是同一把锁,否则无法处理
synchronized (lock) {
if(num > 0) {
String name = Thread.currentThread().getName();
System.out.println(name + "购买了第" + num + "张票");
num--;
}else {
break;
}
}
}
}
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket, "a");
Thread thread2 = new Thread(ticket, "b");
Thread thread3 = new Thread(ticket, "c");
thread1.start();
thread2.start();
thread3.start();
}
}
需要注意的是,Thread.sleep 在线程休眠的时候是不会去释放锁的
同步锁,加锁和释放锁都是自动完成,而不需要程序员手动进行处理。
ReentrantLock
package com.zlt.threads;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable{
private int num = 10;//表示有十张票
//如果设置为true表示是公平锁,谁等的久,就轮到谁,如果不加参数或者为false就是非公平锁
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while(true) {
//锁需要加在指定的对象上面,要确保多线程访问的是同一把锁,否则无法处理
lock.lock();//在这里加锁
if(num > 0) {
String name = Thread.currentThread().getName();
System.out.println(name + "购买了第" + num + "张票");
num--;
}else {
break;
}
lock.unlock();//在这个地方释放锁
}
}
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket, "a");
Thread thread2 = new Thread(ticket, "b");
Thread thread3 = new Thread(ticket, "c");
thread1.start();
thread2.start();
thread3.start();
}
}
ReentrantLock.tryLock() 尝试去加锁,如果没有加上锁就不加了,也可以传入超时时间,如果指定的时间内还没有加上锁,就不再等待
死锁
死锁发生的四个条件:
-
互斥条件,一个资源每次只能被一个线程使用,独木桥只能走一个人
-
请求与保持条件,一个线程因请求资源而阻塞,对已获得的资源保持不放, 甲不退出桥面,已也不退出桥面
-
不可剥夺条件,线程已经获取的资源,在未使用完成之前,不能强行剥夺,甲不能强制乙退,乙也不能强制甲退。
-
循环等待条件,若干个线程形成一个首尾相接的循环等待资源的关系,甲不退出,乙过不了,乙不退出,甲也过不了。
上述的图中简单的描述了一个死锁的成因
死锁的demo
package com.zlt.thread;
public class DeadLock {
private Object lockA = new Object();
private Object lockB = new Object();
Thread threadA = new Thread() {
public void run() {
synchronized (lockA) {
System.out.println("A锁住A");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println("A锁住B");
}
}
};
};
Thread threadB = new Thread() {
public void run() {
synchronized (lockB) {
System.out.println("B锁住B");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockA) {
System.out.println("B锁住A");
}
}
};
};
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
deadLock.threadA.start();
deadLock.threadB.start();
}
}
如果使用ReentrantLock 可以使用唤醒的方式解决死锁问题
package com.zlt.threads;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDeadLockDemo {
public static void main(String[] args) {
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
Thread thread1 = new Thread(new DeadLock(lock1, lock2), "线程1");
Thread thread2 = new Thread(new DeadLock(lock2, lock1), "线程2");
thread1.start();
thread2.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread1.interrupt();
}
}
class DeadLock implements Runnable{
private ReentrantLock lock1;
private ReentrantLock lock2;
public DeadLock(ReentrantLock lock1, ReentrantLock lock2) {
super();
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
try {
//这个锁是可以被唤醒的 唤醒后会抛出异常
lock1.lockInterruptibly();
Thread.sleep(100);
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "正常执行");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "被唤醒了");
} finally {
lock1.unlock();
lock2.unlock();
}
}
}
如何避免死锁?
在有些情况下死锁是可以避免的,介绍三种情况避免死锁:
- 加锁顺序(调整加锁的顺序)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并且释放当前占用的锁)
- 死锁检测
通知和等待
在类Object中,有一些方法被子类继承了下来,这些方法在任何一个对象中都是存在的。
其中有五个方法是跟线程相关的,分别是
wait() 在一个线程中,调用了wait() ,线程会处于等待状态,直到调用了notify或者notifyAll方法通知以后才可以继续执行。如果不通知就一直等待。注意,需要是同一个对象的方法
wait(s) 在上面的基础上,如果没有通知,则达到指定的时间后,也会继续执行,而不会一直等待下去
wait(s,s1); 和一个参数的是一样的效果
notify() 通知,如果在当前对象等待的线程过多,随机唤醒
notifyAll(); 通知,如果在当前对象等待的线程过多,唤醒所有线程,然后所有的线程再一起去竞争资源
wait称为等待,notify 是通知
demo 两个线程交替输出字母和数字
package com.zlt.threads;
public class Test {
private Object lock = new Object();
/**
* 这个线程输出数字
*/
private Thread thread = new Thread() {
public void run() {
for (int i = 1; i <= 26; i++) {
synchronized (lock) {
lock.notify();
System.out.println(i);
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
/**
* 这个线程输出字母
*/
private Thread thread2 = new Thread() {
public void run() {
for (char i = 'A'; i <= 'Z'; i++) {
synchronized (lock) {
lock.notify();
System.out.println(i);
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
public static void main(String[] args) {
Test test = new Test();
test.thread.start();
test.thread2.start();
}
}
通知和等待都需要添加在同步锁中,否则会抛出异常IllegalMonitorStateException
线程池
在实际使用线程的时候,目前只要使用线程,就会涉及一个线程的完整生命周期,会涉及频繁的创建和销毁线程,创建与销毁从来都是最占用资源的。
线程池可以解决这个问题。事先先创建一些线程,放在那里,当有任务来的时候,从这些线程中取一个线程来执行任务,有其他任务来的时候再去取其他的线程来执行任务。如果所有的线程都在执行任务,则涉及扩容或者等待,直到有线程提交了任务,出现了空闲线程等待的任务才会继续执行。
public ThreadPoolExecutor(int corePoolSize, 核心线程数 编制数量
int maximumPoolSize, 最大线程数 工位数量
long keepAliveTime, 等待时间 线程空闲的时间,如果超过这个时间就会停止这个线程
TimeUnit unit, 等待单位
BlockingQueue<Runnable> workQueue, 任务队列 放任务的桌子
ThreadFactory threadFactory, 线程工厂 人事部
RejectedExecutionHandler handler) 拒绝策略 如何拒绝
排队有三种通用策略:
直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
ThreadFactory 线程工厂 负责创建线程
默认情况下使用 Executors.defaultThreadFactory() 创建的线程是非守护线程 优先级为5
RejectedExecutionHandler
RejectedExecutionHandler 的 RejectedExecutionHandler.rejectedExecution(java.lang.Runnable, java.util.concurrent.ThreadPoolExecutor) 方法。下面提供了四种预定义的处理程序策略:
ThreadPoolExecutor.AbortPolicy
用于被拒绝任务的处理程序,它将抛出 RejectedExecutionException.
ThreadPoolExecutor.CallerRunsPolicy
用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。
ThreadPoolExecutor.DiscardOldestPolicy
用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试 execute;如果执行程序已关闭,则会丢弃该任务。
ThreadPoolExecutor.DiscardPolicy
用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
如果上述的4种策略无法满足需求可以自行实现接口RejectedExecutionHandler 重写拒绝方法
需要记忆的方法
package com.zlt.threads;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPool {
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(15, 20, 1, TimeUnit.MINUTES,
new ArrayBlockingQueue<Runnable>(150), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
//提交任务 参数是一个Runnable对象 如果线程数量已经达到最大,并且任务队列满了就会执行拒绝策略
// poolExecutor.execute(command);
//提交任务 但是可以提交Runnable和Callable 可以得到任务执行的反馈
Future<Integer> submit = poolExecutor.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// TODO Auto-generated method stub
return null;
}
});
// submit.get()
//立即结束线程池,未执行完成的任务,返回回来
List<Runnable> shutdownNow = poolExecutor.shutdownNow();
// 不会立刻停止线程池,而是需要把任务执行完毕
poolExecutor.shutdown();
//发起一个等待,在等待时间之内,当前线程不会继续往下,线程池执行完毕后,才会继续往下执行,或者等待的时间到了也会继续往下执行
// poolExecutor.awaitTermination(timeout, unit)
System.out.println();
//获取活动线程数
int activeCount = executor.getActiveCount();
//批量提交任务的 参数是集合
// executor.invokeAll(tasks)
//执行一个,成功执行一个后,其他的任务取消
// executor.invokeAny(tasks)
//线程池是否关闭
boolean shutdown = executor.isShutdown();
//关闭线程池,但是会等任务执行完毕 但不会接受任何新任务
executor.shutdown();
//尝试停止所有主动执行的任务,停止等待任务的处理,并返回正在等待执行的任务列表。
List<Runnable> shutdownNow = executor.shutdownNow();
// 移除任务
// executor.remove(task)
}
}
内置的线程池
缓存线程池,每个任务来的时候如果没有空闲线程都会创建新的线程,当线程空闲超过60s,这个线程会被销毁,一般要求短时间内处理大量的任务时可以使用
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
固长线程池,线程池中的数量是固定的,不会多创建,排队的队列是无边界的
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);
单线程池,相当于固长线程池设置为1
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
延时线程池
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
//等待delay的时间后再去执行一次这个任务
// newScheduledThreadPool.schedule(command, delay, unit)
//在等待initialDelay 后第一次执行任务,然后在initialDelay+period后再执行一次,initialDelay+2*period 再执行一次以此类推
// newScheduledThreadPool.scheduleAtFixedRate(command, initialDelay, period, unit)
缓存线程池,每个任务来的时候如果没有空闲线程都会创建新的线程,当线程空闲超过60s,这个线程会被销毁,一般要求短时间内处理大量的任务时可以使用
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
固长线程池,线程池中的数量是固定的,不会多创建,排队的队列是无边界的
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);
单线程池,相当于固长线程池设置为1
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
延时线程池
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
//等待delay的时间后再去执行一次这个任务
// newScheduledThreadPool.schedule(command, delay, unit)
//在等待initialDelay 后第一次执行任务,然后在initialDelay+period后再执行一次,initialDelay+2*period 再执行一次以此类推
// newScheduledThreadPool.scheduleAtFixedRate(command, initialDelay, period, unit)