1、java中实现多线程
方法一
在java中负责线程的这个功能的是java.lang.Thread这个类
可以通过创建Thread的实例来创建新的线程
每个线程都是通过某个特定的Thread对象所对应的方法run()来完成其操作,方法run()称为线程体
通过调用Thread类的start()方法来启动一个线程
/**
* 实现多线程的时候
* 1、需要继承Thread类
* 2、必须重写run方法,指的是核心执行逻辑
* 3、线程在启动的时候,不要直接调用run()方法,而是要通地start()来进行调用
* 4、每次运行相同的代码,出来的结果可能不一样,原因在于多线程谁先抢占资源无法进行人为控制
*/
public class Test extends Thread {
@Override
public void run() {
for(int i = 0; i< 20; i++) {
System.out.println(Thread.currentThread().getName() + "--------------" + i);
}
}
public static void main(String[] args) {
Test t = new Test();
t.start();
for(int i = 0; i<10; i++) {
System.out.println(Thread.currentThread().getName() + "==============" + i);
}
}
}
方法二
/**
* 实现多线程的时候
* 1、实现Runnable接口
* 2、必须重写run方法,指的是核心执行逻辑
* 3、创建Thread对象,将刚刚创建好的Runnable子类实现作Thread的构造函数
* 4、通过thread的实例化对象调用start()方法进行调用启动
*/
public class Test implements Runnable {
@Override
public void run() {
for(int i = 0; i< 20; i++) {
System.out.println(Thread.currentThread().getName() + "--------------" + i);
}
}
public static void main(String[] args) {
Runnable t = new Test();
Thread th = new Thread(t);
th.start(); //需要先启动进程,这样再执行其他代码才会并行
for(int i = 0; i<10; i++) {
System.out.println(Thread.currentThread().getName() + "==============" + i);
}
}
}
注意:推荐使用第二种方法,在使用runnable接口后不需要给共享变量添加static关键字, 每次创建一个对象,作为共享对象即可, 同时可以避免单继承,把extends留给更需要的继承的地方
方法三
需要实现接口callable
package site.ieven;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for(int i = 0, len = 100; i < len; i ++) {
System.out.println("this is call" + i);
}
return "complete";
}
}
放到线程中执行
package site.ieven; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class FirstJava { public static void main(String[] args) throws ExecutionException, InterruptedException { Callable<String> mc = new MyCallable(); // callable类需要放到FutureTask中执行, FutureTask已实现了Runnable的接口 FutureTask<String> ft = new FutureTask<>(mc); Thread th = new Thread(ft); th.start(); // 返回的是callable中call方法返回的值 System.out.println(ft.get()); } }
注意:线程的运行结果需要在线程执行完成后获取,如果线程没有执行完成,那么则无法获取结果
2、线程的生命周期:
1、新生状态:
当创建好当前线程对象之后,没有启动之前(调用start方法之前)
ThreadDemo thread = new ThreadDemo()
RunnableDemo run = new RunnableDemo()
2、就绪状态:准备开始执行,并没有执行,表示调用start方法之后
当对应的线程创建完成,且调用start方法之后,所有的线程会添加到一个就绪队列中,所有的线程同时去抢占cpu的资源
3、运行状态:当当前进程获取到cpu资源之后,就绪队列中的所有线程会去抢占cpu的资源,谁先抢占到谁先执行,在执行的过程中就叫做运行状态
抢占到cpu资源,执行代码逻辑开始
4、死亡状态:当运行中的线程正常执行完所有的代码逻辑或者因为异常情况导致程序结束叫做死亡状态
进入的方式:
1、正常运行完成且结束
2、人为中断执行,比如使用stop方法
3、程序抛出未捕获的异常
5、阻塞状态:在程序运行过程中,发生某些异常情况,导致当前线程无法再顺利执行下去,此时会进入阻塞状态,进入阻塞状态的原因消除之后,
所有的阻塞队列会再次进入到就绪状态中,随机抢占cpu的资源,等待执行
进入的方式:
sleep方法
等待io资源
join方法(代码中执行的逻辑)
注意:
在多线程的时候,可以实现唤醒和等待的过程,但是唤醒和等待操作的对应不是thread类
而是我们设置的共享对象或者共享变量
多线程并发访问的时候回出现数据安全问题:
解决方式:
1、同步代码块
synchronized(共享资源、共享对象,需要是object的子类){具体执行的代码块}
2、同步方法
将核心的代码逻辑定义成一个方法,使用synchronized关键字进行修饰,此时不需要指定共享对象
3、进程操作的相关函数
public class Test implements Runnable {
@Override
public void run() {
for(int i = 0; i< 20; i++) {
System.out.println(Thread.currentThread().getName() + "--------------" + i);
}
}
public static void main(String[] args) {
Runnable t = new Test();
Thread th = new Thread(t);
th.start(); //需要先启动进程,这样再执行其他代码才会并行
//获取指定进程的名称
System.out.println(th.getName());
//获取指定进程的id
System.out.println(th.getId());
//获取指定进程的优先级,默认是5,值越大,被执行的可能性就越大
System.out.println(th.getPriority());
//设置指定进程的优先级,通常是1-10, Thread.MAX_PRIORITY Thread.MIN_PRIORITY Thread.NORM_PRIORITY
th.setPriority(10);
//获取指定进程的描述
System.out.println(th.getState());
//判断指定进程是否存活
System.out.println(th.isAlive());
//获取当前进程即哪个先运行,那么打印的就是哪个进程
System.out.println(Thread.currentThread());
for(int i = 0; i<10; i++) {
System.out.println(Thread.currentThread().getName() + "==============" + i);
}
}
}
public class Test implements Runnable {
@Override
public void run() {
for(int i = 0; i< 10; i++) {
System.out.println(Thread.currentThread().getName() + "-----------"+ i);
}
}
public static void main(String[] args) {
Runnable r = new Test();
Thread th = new Thread(r);
th.start();
// th.stop(); //直接终止线程,不推荐使用
try {
// th.join(); //相当指定线程强制执行,其他线程处于阻塞状态,该线程执行完成后,其他线程再继续执行
// System.out.println(th.getName());
Thread.sleep(1000); //表示在哪个线程里运行的,那么那个线程就进行休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i = 0; i< 5; i++) {
System.out.println(Thread.currentThread().getName() + "==============" + i);
if(i == 0) {
Thread.yield(); //表示当前正在执行的线程暂停一次,允许其他线程执行,不阻塞,线程进入就绪状态,如果没有其他等待的程序,这时候当前程序就会立马恢复执行
System.out.println(Thread.currentThread().getName() + "休息一回合");
}
}
}
}
注意:这里的Thread.sleep与Thread.yield两个方法是Thread的静态方法,在哪个线程体内调用,那么这两个方法就作用在哪个线程中;通俗的讲反正在哪一个线程里面执行了sleep()或yield()方法哪一个线程就休眠或暂停。
以一个实例带入线程安装问题:
在多线程在运行过程中,如果出现多个线程操作一份数据,那么就有可能产生线程的安全问题
举例:某电影院上映一部电影,该电影共有100张票,而它有3个窗口在卖票, 请设计一个程序模拟该电影院卖票
根据上面的例子,编码如下:
MyRunable
package site.ieven;
public class MyRunable implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if (this.ticket <= 0) break;
try {
//正在办理手续的时间
Thread.sleep(100);
this.ticket--;
System.out.println(Thread.currentThread().getName() + "正在卖票,当前余票还有 " + this.ticket + "张!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
调用代码
package site.ieven;
public class FirstJava {
public static void main(String[] args) throws InterruptedException {
MyRunable mr = new MyRunable();
// 窗口一
Thread th1 = new Thread(mr);
th1.setName("窗口1");
// 窗口二
Thread th2 = new Thread(mr);
th2.setName("窗口2");
// 窗口三
Thread th3 = new Thread(mr);
th3.setName("窗口3");
th1.start();
th2.start();
th3.start();
}
}
这个时间多个线程同时操作一份数据,那么就会产生线程安全问题,导致票数重复或者小于0的负数情况,这个时候就需要线程锁进行操作, 改造如下:
package site.ieven;
public class MyRunable implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
synchronized (this) {
if (this.ticket <= 0) break;
try {
//正在办理手续
Thread.sleep(100);
this.ticket--;
System.out.println(Thread.currentThread().getName() + "正在卖票,当前余票还有 " + this.ticket + "张!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
注意:1、synchronized放在while里面,是因为是需要在循环的基础上,并且,里面的逻辑需要在线程锁的情况下运行;
2、锁对象需要是唯一的,比如上面例子上的this,在调用的时候myRunable始终是唯一的一个类
另一种实现方式
MyRunable
package site.ieven;
public class MyRunable implements Runnable {
private static int ticket = 100;
private final static Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (MyRunable.ticket <= 0) break;
try {
//正在办理手续
Thread.sleep(100);
MyRunable.ticket--;
System.out.println(Thread.currentThread().getName() + "正在卖票,当前余票还有 " + MyRunable.ticket + "张!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
调用
package site.ieven;
public class FirstJava {
public static void main(String[] args) throws InterruptedException {
// 窗口一
MyRunable mr1 = new MyRunable();
Thread th1 = new Thread(mr1);
th1.setName("窗口1");
// 窗口二
MyRunable mr2 = new MyRunable();
Thread th2 = new Thread(mr2);
th2.setName("窗口2");
// 窗口三
MyRunable mr3 = new MyRunable();
Thread th3 = new Thread(mr3);
th3.setName("窗口3");
th1.start();
th2.start();
th3.start();
}
}
除了同步代码块,如上所示,还有同步方法,以及同步静态方式
// 同步代码块
synchronized(obj) { ...逻辑代码 } //锁对象可以是用户定义的对象也可以是this
// 同步方法
public synchronized void testMethod() { ...逻辑代码 } // 锁对象是this
// 同步静态方法
public static synchronized void testMethod() { ...逻辑代码 } // 锁对象是 类名.class
Lock锁
代码中的锁是自动开关的,在jdk5以后提供了新的对象Lock, 但是Lock是一个接口,因此我们需要采用它的实现类ReentrantLock来实例化
MyRunnable改造如下
package site.ieven;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyRunable implements Runnable {
private static int ticket = 100;
private final static Object obj = new Object();
private final Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
try {
if (MyRunable.ticket <= 0) break;
//正在办理手续
Thread.sleep(100);
MyRunable.ticket--;
System.out.println(Thread.currentThread().getName() + "正在卖票,当前余票还有 " + MyRunable.ticket + "张!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
package site.ieven;
public class FirstJava {
public static void main(String[] args) throws InterruptedException {
Object obj1 = new Object();
Object obj2 = new Object();
// 线程一
new Thread(() -> {
while(true) {
synchronized (obj1) {
synchronized (obj2) {
System.out.println("小康同学在走路");
}
}
}
}).start();
// 线程二
new Thread(() -> {
while(true) {
synchronized (obj2) {
synchronized (obj1) {
System.out.println("小微同学在走路");
}
}
}
}).start();
}
}
守护线程
当把一个线程设置为守护线程,那么当普通线程执行完成后,那么守护线程也就没的执行下去的必要,但是在下达停止命令时还会继续向前执行一小会,如果需要把一个线程设置为守护线程,那么需要用以下方法
Thread th = new Mythead()
th.setDaemon(true);
4、生产者与消费者
生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻
等待与唤醒的方法
为了体现生产和消费过程中的等待和唤醒,java就提供了几个方法供我们使用,这几个方法在Object类中
void await() // 导致当前线程等待, 直到另一个线程调用该对象的notify()方法或notifyAll()方法
void notify() // 唤醒正在等待对象监视器的单个线程
void notifyAll() // 唤醒正在等待对象监视器的所有线程
5、BlockingQueue类的使用
具体查看相关博客: https://blog.csdn.net/qq_42135428/article/details/80285737
相应的例子
public class Goods { private String brand; private String type; public Goods() {} public Goods(String brand, String type) { this.brand = brand; this.type = type; System.out.println("生产了" + this.toString()); } @Override public String toString() { return "【商品名称: "+ this.brand + ", 商品种类: " + this.type + "】"; } }
public class Produce implements Runnable{ private final BlockingQueue<Goods> queue; public Produce (BlockingQueue<Goods> queue) { this.queue = queue; } @Override public void run() { for(int i = 0; i< 20; i++) { try { Goods goods = new Goods("康师傅", "冰红茶"); this.queue.put(goods); System.out.println("生产了" + goods.toString() + "现在的数量是" + this.queue.size()); } catch (InterruptedException e) { e.printStackTrace(); } } } }
public class Consume implements Runnable { private final BlockingQueue<Goods> queue; public Consume (BlockingQueue<Goods> queue) { this.queue = queue; } @Override public void run() { for(int i = 0; i< 18; i++) { try { Goods goods = this.queue.take(); System.out.println("消费了" + goods.toString() + "现在余下" + this.queue.size()); } catch (InterruptedException e) { e.printStackTrace(); } } } }
//在调用take与put的时候,类的内部已做了等待
public class Test { public static void main(String[] args) { BlockingQueue<Goods> goodsList = new ArrayBlockingQueue<>(10); Thread pth = new Thread(new Produce(goodsList)); Thread cth = new Thread(new Consume(goodsList)); pth.start(); cth.start(); } }
5、线程池分类
ThreadPollExecutor的使用
public class Task implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + "---running"); } }
public class Test { public static void main(String[] args) { //创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 ExecutorService executorService = Executors.newCachedThreadPool(); //创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 ExecutorService executorService = Executors.newFixedThreadPool(10); //创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行 ExecutorService executorService = Executors.newSingleThreadExecutor(); for(int i = 0; i < 2000; i ++) { executorService.execute(new Task()); } executorService.shutdown(); } }
ScheduledExecutorService的使用
public class Test { public static void main(String[] args) { ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5); //表示延时固定的时间后执行一次 scheduledExecutorService.schedule(new Task(), 3, TimeUnit.SECONDS); //表示循环的延时固定的时间执行一次,下面代码表示,每三秒,延时一秒执行一次 scheduledExecutorService.scheduleAtFixedRate(new Task(), 1, 3, TimeUnit.SECONDS); //该方法与上个方法的区别在于,该方法表示上个任务执行完成后再进行延迟,而上个方法表示在上个任务开始时进行计时 scheduledExecutorService.scheduleWithFixedDelay(new Task(), 1, 3, TimeUnit.SECONDS); // scheduledExecutorService.shutdown(); //这里注意要注释掉 } }
注意:Executors.newScheduleThreadPool与Executors.newSingleThreadScheduledExecutor的区别在于后者是单线程操作
ForkJoinPool的使用
ForkJoinPool 是 JDK1.7 开始提供的线程池。为了解决 CPU 负载不均衡的问题。如某个较大的任务,被一个线程去执行,而其他线程处于空闲状态。
ForkJoinTask 表示一个任务,ForkJoinTask 的子类中有 RecursiveAction 和 RecursiveTask。
RecursiveAction 无返回结果;RecursiveTask 有返回结果。
重写 RecursiveAction 或 RecursiveTask 的 compute(),完成计算或者可以进行任务拆分
package com.yfbill.test; import java.util.concurrent.RecursiveAction; public class CopyTask extends RecursiveAction { private static final int SPLIT_NUM = 49; private final int start; private final int end; public CopyTask (int start, int end) { this.start = start; this.end = end; } @Override protected void compute() { if(this.end - this.start < CopyTask.SPLIT_NUM) { for(int i = start; i <= end; i++) { System.out.println(Thread.currentThread().getName() + "----------" + i); } } else { int middle = (start + end) / 2; CopyTask left = new CopyTask(start, middle); CopyTask right = new CopyTask(middle + 1, end); CopyTask.invokeAll(left, right); // left.fork(); // right.fork(); } } }
调用
public class Test { public static void main(String[] args) { ForkJoinPool forkJoinPool = new ForkJoinPool(); forkJoinPool.submit(new CopyTask(1, 200)); // forkJoinPool.shutdown(); try { forkJoinPool.awaitTermination(2, TimeUnit.SECONDS); forkJoinPool.shutdown(); } catch (InterruptedException e) { e.printStackTrace(); } } }
用法,一般用在可分组,并且可自行拆分调用的方法里面执行
6、线程池的生命周期
-
RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务;
-
SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻 塞队列中已保存的任务。
-
STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的 线程。
-
TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0, 线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
-
TERMINATED:在terminated() 方法执行完后进入该状态,默认 terminated()方法中什么也没有做
7、线程池的创建
参数说明
▪ corePoolSize:核心线程池的大小
▪ maximumPoolSize:线程池能创建线程的最大个数
▪ keepAliveTime:空闲线程存活时间
▪ unit:时间单位,为keepAliveTime指定时间单位
▪ workQueue:阻塞队列,用于保存任务的阻塞队列
▪ threadFactory:创建线程的工程类
▪ handler:饱和策略(拒绝策略)
注意:execute与submit的区别在于,前者是没有返回值的,而后者是有返回值 ,并且前都接收Runnable的实现类,而后者接收callable的实现类,但后者也可以使用runnable来实现