Java---多线程
一、多线程
1.线程与进程
- 进程
是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
线程 - 线程
是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进程最少
有一个线程
线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分
成若干个线程
2.线程调度
- 分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),
Java使用的为
- 抢占式调度
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,
只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时
刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使
用率更高。
3.并发与并行
-
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
-
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
4.线程中断
//添加中断标记
thread.interrupt();
代码如下:
public static void main(String[] args) {
Runnable myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
//添加中断标记
thread.interrupt();
}
static class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//发现中断标记会进入catch方法,直接return,会终止线程
return;
}
}
}
}
}
5.守护线程
当最后一个用户线程死亡时,所有守护线程自动死亡
设置守护线程需在线程启动之前设置。
Runnable myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.setDaemon(true);
thread.start();
二、创建线程的三种方式
1、继承Thread类
编写一个类myThread 继承Thread类;
在main方法中创建这个类的对象,调用start()方法,启动线程;
public class Demo5 {
public static void main(String[] args) {
Thread myThread = new myThread();
myThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("我是main线程"+i);
}
}
static class myThread extends Thread {
@Override
public void run() {
//这里表示一条新的执行路径
//这个执行方式发触发方式,不是调用run方法,而是通过这个继承了Thread类的对象调用start方法启动任务
for (int i = 0; i < 10; i++) {
System.out.println("我是myThread线程"+i);
}
}
}
}
运行main方法,可以看到Java线程的抢占式运行;
2、实现Runnable接口
1、编写一个类myThread 实现Runable接口;
在main方法中创建这个类的对象,即一个线程任务,创建Thread对象调用start()方法执行该任务,启动线程;
public class Demo5 {
public static void main(String[] args) {
Runnable myThread = new myThread();
Thread thread = new Thread(myThread);
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println("我是main线程"+i);
}
}
static class myThread implements Runnable {
@Override
public void run() {
//这里表示一条新的执行路径
//这个执行方式发触发方式,不是调用run方法,而是通过这个继承了Thread类的对象调用start方法启动任务
for (int i = 0; i < 10; i++) {
System.out.println("我是实现Runable的线程"+i);
}
}
}
}
运行main方法,可以看到Java线程的抢占式运行;
2、该步骤可以使用匿名类的方式实现,代码如下:
public class Demo5 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我是实现Runable的线程"+i);
}
}
});
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println("我是main线程"+i);
}
}
}
3、实现Callable接口
该接口的使用步骤
//1.编写类实现Callable接口 ,实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
//2.创建FutureTask对象 ,并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);
//3.通过Thread,启动线程
new Thread(future).start();
使用实例:
public class Demo6 {
public static void main(String[] args) {
Callable<Integer> c = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(c); //创建FutureTask对象 , 并传入第一步编写的Callable类对象
new Thread(task).start(); //通过Thread,启动线程
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
static class MyCallable implements Callable<Integer> { //编写类实现Callable接口 , 实现call方法
@Override
public Integer call() throws Exception {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
return 100;
}
}
}
4、Runnable和Callable的区别
1)接口定义:
//Callable接口
public interface Callable {
V call() throws Exception;
}
//Runnable接口
public interface Runnable {
public abstract void run();
}
2) 不同点
- 两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
3)注意点
Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!
三、线程安全问题
先看一段线程不安全代码实例:
public class Demo5 {
public static void main(String[] args) {
Runnable run = new Ticket();
//创建三个线程
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable {
//票数
private int count = 10;
@Override
public void run() {
while (count > 0) {
//买票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票" + count);
}
}
}
}
执行结果:
上段代码意义为对同一个卖票任务同时启动了三个线程窗口进行卖票。可以看到出现余票负数的情况,按照代码逻辑,不可能出现负数,但是由于线程的抢占机制,三个线程是并发执行的,就会产生这种线程不安全的现象。
1、同步代码块-线程安全
对需要执行的任务使用synchronized 关键字进行上锁,上述问题可以通过同步代码块的方式实现线程安全:
public class Demo5 {
public static void main(String[] args) {
Runnable run = new Ticket();
//创建三个线程
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable {
//票数
private int count = 10;
//创建一个锁对象
private Object lock = new Object();
@Override
public void run() {
//使用synchronized关键字对任务进行上锁
synchronized (lock) {
while (true) {
if (count == 0) {
break;
}
//买票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票" + count);
}
}
}
}
}
可以看到控制台打印正常了,没有出现负数的情况。
2、同步方法-线程安全
将需要执行的任务放在一个单独的方法中,使用synchronized 关键字修饰该方法,实现与同步代码快一样的效果,代码如下:
public class Demo5 {
public static void main(String[] args) {
Runnable run = new Ticket();
//创建三个线程
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable {
//票数
private int count = 10;
//创建一个锁对象
private Object lock = new Object();
@Override
public void run() {
show();
}
private synchronized void show() {
while (true) {
if (count == 0) {
break;
}
//买票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票" + count);
}
}
}
}
3、 显式锁–线程安全
同步代码块和同步方法都属于隐式锁,创建显示锁的方式:
//创建显示锁对象
Lock l = new ReentrantLock();
(当传入参数为true时,为公平锁,默认false)
//当有线程进来时,对任务上锁
l.lock();
//当该线程结束时,释放锁
l.unlock();
代码示例:
public class Demo5 {
public static void main(String[] args) {
Runnable run = new Ticket();
//创建三个线程
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable {
//票数
private int count = 10;
//创建显示锁对象
Lock l = new ReentrantLock();
@Override
public void run() {
//使用显式锁对任务进行上锁
while (true) {
//当有线程进来时,对任务上锁
l.lock();
if (count == 0) {
break;
}
//买票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("出票成功,余票" + count);
//当该线程结束时,释放锁
l.unlock();
}
}
}
}
4、线程死锁
-
死锁定义
所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
-
产生死锁的必要条件
互斥条件:线程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个线程所占有。此时若有线程请求该资源,则请求线程只能等待。
不剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程倾向夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该线程已被其他线程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。即存在一个处于等待状态的线程集合{P1,P2,…,Pn},其中Pi等待的资源被P(i+1)占有(i=0,1,…,n-1),Pn等待的资源被P0占有
死锁示例:
/**
* 一个简单的死锁类
* 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500ms
* 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500ms
* td1 睡眠结束后需要锁定 o2 才能继续执行,而此时 o2 已被 td2 锁定;
* td2 睡眠结束后需要锁定 o1 才能继续执行,而此时 o1 已被 td1 锁定;
* td1、td2 相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
*/
public class DeadLock implements Runnable {
public int flag = 1;
//静态对象是类的所有对象共享的
private static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
System.out.println("flag="+flag);
if(flag==1){
synchronized (o1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println("1");
}
}
}
if(flag==0){
synchronized (o2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag=1;
td2.flag=0;
//td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
//td2的run()可能在td1的run()之前运行
new Thread(td1).start();
new Thread(td2).start();
}
}
四、线程池
线程池的好处
- 降低资源消耗。
- 提高响应速度。
- 提高线程的可管理性。
Java中的四种线程池 概述
1. 缓存线程池
/**
* 缓存线程池
* (长度无限制)
执行流程:
判断线程池是否存在空闲线程
存在则使用
不存在,则创建线程 并放入线程池, 然后使用
*/
public class Demo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
//指挥线程池执行新的任务
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
}
);
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
}
);
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
}
);
//休眠后线程进入空闲状态
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
}
);
}
2、定长线程池
public class Demo2 {
/**
* 定长线程池
* (长度是指定的数值)
*执行流程:
*判断线程池是否存在空闲线程
*存在则使用
*不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
*不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*/
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
}
}
3、单线程线程池
效果与定长线程池 创建时传入数值1 效果一致.
单线程线程池.
执行流程:
- 判断线程池 的那个线程 是否空闲
- 空闲则使用
- 不空闲,会等待池中的单个线程空闲后使用
///新建一个单线线程池对象 newSingleThreadExecutor();
ExecutorService service = Executors.newSingleThreadExecutor();
4、周期性线程池
周期任务 定长线程池.
执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
public class Demo4 {
/**
* 周期定长线程池
*/
public static void main(String[] args) {
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/**
* 1.定时执行一次
* arg1 定时执行的任务
* arg2 时长数字
* arg3 时长数字的时间单位
*/
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("锄禾日当午");
}
},5, TimeUnit.SECONDS);
/**
* 周期性执行任务
* arg1 任务
* arg2 延迟时长数字
*arg3 周期时长数字(每隔多久执行一次)
* arg4 时长数字的单位
*/
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("汗滴禾下土");
}
},5,1,TimeUnit.SECONDS);
}
}