java多线程(详解)
线程-概述
线程(Thread)是一个程序内部的一条执行流程。
public static void main(String[] args) {
//代码
for(int i=0;i<10; i++){
System.out.println(i);
}
//代码
}
上面的main方法就是一个线程,程序中如果只有一条执行流程,那这个程序就是单线程的程序
那么多线程是什么呢?
多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行);
多线程一般都用在我们生活的方方面面,比如12306买车票,每次都是同时有很多人一起访问,所以就会设计多线程,性能更加的好;还有消息通信,淘宝,京东系统都离不开多线程技术
如何在程序中创建出多条线程? Java是通过java.lang.Thread类的对象来代表线程的。
线程的创建有三种方式:
创建方式一:继承Thread类,实现run 方法
- 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
- 创建MyThread类的对象
- 调用线程对象的start() 方法启动线程(启动后还是执行run方法的)
//继承Thread类
public class MyThread extends Thread{
@Override
public void run() {
// super.run();
for (int i = 0; i < 5; i++) {
System.out.println("新的线程"+i);
}
}
}
//测试类 ,启用线程
public class demo {
public static void main(String[] args) {
//第一种创建线程的方式
//导致不可以继承其他的类了
//main是由一条默认的主线程进行执行的
// 创建MyThread线程类的对象代表一个线程
Thread t1=new MyThread(); //多态写法
//启动线程
t1.start();
for (int i = 0; i <5 ; i++) {
System.out.println("main主线程"+i);
}
}
}
/*执行结果:
main主线程0
main主线程1
main主线程2
main主线程3
新的线程0
新的线程1
新的线程2
新的线程3
新的线程4
main主线程4
*/
执行结果每次都是不一样的,因为每个线程都是自己抢资源运行
方式一优缺点:
- 优点:编码简单
- 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。
多线程的注意事项:
-
启用线程必须是调用Start方法,不是调用run 方法。
- 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行
- 只有调用start 方法才是启动一个新的线程执行
-
不要把主线程任务放在启动子线程之前
- 这样主线程一直都是先跑完,相当于是一个单线程的效果了。
创建方式二:实现Runnable接口
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
- 创建MyRunnable任务对象
- 把MyRunnable任务对象交给Thread处理。
| Thread类提供的构造器 | 说明 |
|---|---|
| public Thread(Runnable target) | 封装Runnable对象成为线程对象 |
调用线程对象的start()方法启动线程
public class mythread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("新的线程"+i);
}
}
}
//测试类
public class demo2 {
public static void main(String[] args) {
// 第二种创建线程方式 实现接口 Runnable
//创建任务对象
Runnable my2=new mythread2(); //多态写法
//把任务对象交个线程对象处理
new Thread(my2).start();
}
}
//执行结果:
/*
新的线程0
新的线程1
新的线程2
新的线程3
新的线程4
我是第二个线程
我是线程3
*/
方式二的优缺点:
- 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
- 缺点:需要多一个Runnable对象(其实也不算缺点相比较于第一种的缺点)
创建方式二的匿名内部类写法、然后也可以进一步简写lambda表达式
//匿名写法,简写
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是第二个线程");
}
}).start();
//简写2. lambda 表达式
new Thread(()->{
System.out.println("我是线程3");
}).start();
创建方式三:实现Callable接口
多线程的第三种创建方式:利用Callable接口、FutureTask类来实现。
-
创建任务对象:
- 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
- 把Callable类型的对象封装成FutureTask(线程任务对象)。
-
把线程任务对象交给Thread对象。
-
调用Thread对象的start方法启动线程。
-
、线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。
| FutureTask提供的码疸命 | 说明 |
|---|---|
| public FutureTask<>(Callable call) | 把callable对象封装成FutureTask对象。 |
| FutureTask提供的方法 | 说明 |
|---|---|
| public v get() throws Exception | 获取线程执行call方法返回的结果。 |
实现Callable 的类
public class myCallable implements Callable<String> {
private int n;
public myCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum=0;
for (int i = 0; i <= n; i++) {
sum+=i; //求1-n的和
}
return "最后的结果为:"+sum;
}
}
测试类
public class demo3 {
public static void main(String[] args) throws Exception {
//第三种创建方式 callable
//创建一个Callable 的对象
Callable<String> c1=new myCallable(100);
String name=Thread.currentThread().getName();
System.out.println(name); //main
//把callable 对象封装成为 FutureTesk 未来任务对象
//FutureTask 实现了Runnable 接口的所以是一个任务对象
FutureTask<String> ft=new FutureTask<>(c1);
// 把任务对象交给一个Thread对象
new Thread(ft).start();
String name1=Thread.currentThread().getName();
System.out.println(name1);//main
//线程执行完了就可以通过FutureTesk中的 get 方法获取线程返回的值了
System.out.println(ft.get()); //最后的结果为:5050
}
}
线程创建方式三的优缺点:
优点∶线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
缺点∶编码复杂一点。
线程-常用方法
Thread提供的常用方法
| Thread提供的常用方法 | 说明 |
|---|---|
| public void run() | 线程的任务方法 |
| public void start() | 启动线程 |
| public string getName() | 获取当前线程的名称,线程名称默认是Thread-索引 |
| public void setName(String name) | 为线程设置名称 |
| public static Thread currentThread() | 获取当前执行的线程对象 |
| public static void sleep(long time) | 让当前执行的线程休眠多少毫秒后,再继续执行 |
| public final void join()... | 让调用当前这个方法的线程先执行完! |
Thread提供的常用构造器
| Thread提供的常见构造器 | 说明 |
|---|---|
| public Thread( String name) | 可以为当前线程指定名称 |
| public Thread(Runnable target) | 封装Runnable对象成为线程对象 |
| public Thread(Runnable target,String name) | 封装Runnable对象成为线程对象,并指定线程名称 |
为线程设置名字和获取线程名字
// 创建MyThread线程类的对象代表一个线程
Thread t1=new MyThread(); //多态写法
t1.setName("1号线程"); //为线程设置名字
System.out.println(t1.getName()); // 1号线程
join方法作用:让当前调用这个方法的线程先执行完
Thread t1=new MyThread();
t1.start();
t1.join();
t2.start(); //例如下面又启动了一个t2线程,这就会让t1线程执行完后,才会开始做任务
interrupt():线程中断
public class ThreadTest08 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable2());
t.setName("t");
t.start();
// 希望5秒之后,t线程醒来(5秒之后主线程手里的活儿干完了。)
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
t.interrupt();
}
}
class MyRunnable2 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---> begin");
try {
// 睡眠1年
Thread.sleep(1000 * 60 * 60 * 24 * 365);
} catch (InterruptedException e) {
e.printStackTrace();
}
//1年之后才会执行这里
System.out.println(Thread.currentThread().getName() + "---> end");
}
yield(): 让位,当前线程暂停,回到就绪状态,让给其他线程
public class ThreadTest12 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable6());
t.setName("t");
t.start();
for(int i = 1; i <= 10000; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
class MyRunnable6 implements Runnable {
@Override
public void run() {
for(int i = 1; i <= 10000; i++) {
//每100个让位一次。
if(i % 100 == 0){
Thread.yield(); // 当前线程暂停一下,让给主线程。
}
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
注意: 并不是每次都让成功的,有可能它又抢到时间片了。
守护线程
java语言中线程分为两大类:
- 一类是:用户线程
- 一类是:守护线程后台线程
其中具有代表性的就是:垃圾回收线程(守护线程) 。
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
注意:主线程main方法是一个用户线程。
守护线程用在什么地方呢?
每天00:00的时候系统数据自动备份。
这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
一直在那里看着,没到00:00的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了。
| 方法名 | 作用 |
|---|---|
| void setDaemon(boolean on) | on 为true时表示吧线程设置为守护线程 |
public class ThreadTest14 {
public static void main(String[] args) {
Thread t = new BakDataThread();
t.setName("备份数据的线程");
// 启动线程之前,将线程设置为守护线程
t.setDaemon(true);
t.start();
// 主线程:主线程是用户线程
for(int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread {
public void run(){
int i = 0;
// 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
while(true){
System.out.println(Thread.currentThread().getName() + "--->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程-线程优先级
常量:
| 常量名 | 备注 |
|---|---|
| static int MAX_PRIORITY | 最高优先级(10) |
| static int MIN_PRIORITY | 最低优先级(1) |
| static int NORM_PRIORITY | 默认优先级(5) |
方法
| 方法名 | 最高优先级(10) |
|---|---|
| int getPriority() | 获得线程优先级 |
| void setPriority(int newPriority) | 设置线程优先级 |
基本使用:
public class ThreadTest11 {
public static void main(String[] args) {
System.out.println("最高优先级:" + Thread.MAX_PRIORITY);//最高优先级:10
System.out.println("最低优先级:" + Thread.MIN_PRIORITY);//最低优先级:1
System.out.println("默认优先级:" + Thread.NORM_PRIORITY);//默认优先级:5
// main线程的默认优先级是:5
System.out.println(hread.currentThread().getName() + "线程的默认优先级是:" + currentThread.getPriority());
Thread t = new Thread(new MyRunnable5());
t.setPriority(10);
t.setName("t");
t.start();
// 优先级较高的,只是抢到的CPU时间片相对多一些。
// 大概率方向更偏向于优先级比较高的。
for(int i = 0; i < 10000; i++){
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
class MyRunnable5 implements Runnable {
@Override
public void run() {
for(int i = 0; i < 10000; i++){
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
注意:
- main线程的默认优先级是:5
- 优先级较高的,只是抢到的CPU时间片相对多一些,大概率方向更偏向于优先级比较高的
线程安全
什么是线程安全问题?
多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
可以通过一个案例来理解
场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?
所以线程安全问题出现的原因就知道了?
- 存在多个线程在同时执行
- 同时访问一个共享资源
- 存在修改该共享资源
用程序模拟线程安全问题
需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。
分析:
①:需要提供一个账户类,接着创建一个账户对象代表2个人的共享账户。
②:需要定义一个线程类(用于创建两个线程,分别代表小明和小红)。
③:创建2个线程,传入同一个账户对象给2个线程处理。
④:启动2个线程,同时去同一个账户对象中取钱10万。
账户类:account
public class Account {
private String cardId; //卡号
private double money; //余额
public Account() {
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
//取钱
public void getAccountmoney(double money){
if(this.money>=money){
//余额够的话就可以实现取钱
String name=Thread.currentThread().getName();
System.out.println(name+"成功取钱!"+money);
this.money-=money;
System.out.println("剩余的余额为"+this.money);
}else{
System.out.println("取钱失败,余额不足"+this.money);
}
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
线程类:
public class mythreads extends Thread{
private Account acc;
public mythreads(Account acc,String name) {
super(name); //设置线程的名字
this.acc=acc;
}
@Override
public void run() {
acc.getAccountmoney(100000);
}
}
测试类;
public class demo33 {
public static void main(String[] args) {
//先创建一个账户对象
Account acc=new Account("icbc-1234",100000);
//创建两个线程共享一个账户取钱
Thread ming=new mythreads(acc,"小明");
Thread hong=new mythreads(acc,"小红");
ming.start();
hong.start();
}
}
运行结果
小红成功取钱!100000.0 小明成功取钱!100000.0 剩余的余额为0.0 剩余的余额为-100000.0
所以需要线程同步来解决
线程同步
线程同步就是解决安全问题的方案
线程同步的思想:让多个线程实现先后依次的访问共享资源,这样就解决了安全问题
线程同步的常用方案就是加锁
加锁: 每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
加锁总共有三种方式:
方式一:同步代码块
作用: 把访问共享资源的核心代码给上锁,以此保证线程安全。
Synchronized(同步锁){
//访问共享资源的核心代码
}
原理: 每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
同步锁的注意事项:
对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象) ,否则会出bug.
下面还是用的上面的线程安全问题改造的加锁,实现线程同步
//取钱
public void getAccountmoney(double money){
synchronized (this){ //多个线程操作一个对象,所以让对象本身作为锁对象,最好
String name=Thread.currentThread().getName();
if(this.money>=money){
//余额够的话就可以实现取钱
this.money-=money;
System.out.println(name+"成功取钱!"+money);
System.out.println("剩余的余额为"+this.money);
}else{
System.out.println(name+"取钱失败!!!");
System.out.println("取钱失败,余额不足"+this.money);
}
}
}
执行结果:
小明成功取钱!100000.0 剩余的余额为0.0 小红取钱失败!!! 取钱失败,余额不足0.0
锁对象随便选择一个唯一的对象好不好呢? 当然是不可以的,会影响其他无关线程的执行
锁对象的使用规范:
- 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
- 对于静态方法建议使用字节码 (类名.class) 对象作为锁对象。
方式二:同步方法
作用: 把访问共享资源的核心方法给上锁,以此保证线程安全。
修饰符 synchronized 返回值类型 方法名称(形参列表){
//操作共享资源的代码
}
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
默认锁对象是this,只是你自己看不到啦
//取钱 //在方法上加锁
public synchronized void getAccountmoney(double money){
if(this.money>=money){
//余额够的话就可以实现取钱
String name=Thread.currentThread().getName();
System.out.println(name+"成功取钱!"+money);
this.money-=money;
System.out.println("剩余的余额为"+this.money);
}else{
System.out.println("取钱失败,余额不足"+this.money);
}
}
执行结果:
小明成功取钱!100000.0 剩余的余额为0.0 小红取钱失败!!! 取钱失败,余额不足0.0
同步方法底层原理
同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
如果方法是实例方法:同步方法默认用this作为的锁对象。
如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
方式三:Lock锁
Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁。更灵活、更方便、更强大
Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象
| 构造器 | 说明 |
|---|---|
| public ReentrantLock() | 获得Lock锁的实现类对象 |
Lock的常用方法:
| 方法名称 | 说明 |
|---|---|
| void lock() | 获得锁 |
| void unlock() | 释放锁 |
基本用法:
先定义锁对象,然后在需要的代码前加锁,一般用捕获,把关锁放在Finally 里面,反之后面线程进不来
private Lock lk=new ReentrantLock(); //创建一个锁对象
//取钱
public void getAccountmoney(double money){
lk.lock(); //加锁
try {
if(this.money>=money){
//余额够的话就可以实现取钱
String name=Thread.currentThread().getName();
System.out.println(name+"成功取钱!"+money);
this.money-=money;
System.out.println("剩余的余额为"+this.money);
}else{
System.out.println("取钱失败,余额不足"+this.money);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//防止前面出现异常,导致后面的线程无法进入
lk.unlock(); //解锁
}
}
线程通信
当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。
线程通信的常见模型(生产者与消费者模型)
- 生产者线程负责生产数据
- 消费者线程负责消费生产者生产的数据。
- 注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产!
程序实现生产消费模型:
场景:3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃。
在这之前我们应该知道:
Object类的等待和唤醒方法:
| 方法名称 | |
|---|---|
| void wait() | 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或notifyAl1()方法 |
| void notify() | 唤醒正在等待的单个线程 |
| void notifyAll() | 唤醒正在等待的所有线程 |
注意:
上述方法应该使用当前同步锁对象(就是的到锁的对象)进行调用
实现场景:
桌子类:
public class Desk {
//桌子用来放包子
//定义一个集合用来放包子
private List<String> list=new ArrayList<>();
//放包子
public synchronized void put(){
try {
String name=Thread.currentThread().getName();
if(list.size()==0){
//桌子上的没有包子,那么厨师就要生成一个包子,放在桌子上
list.add("做个一个肉包子");
System.out.println(name+"做了一个肉包子!!");
Thread.sleep(2000); //做东西要两秒
notifyAll();
wait();
}else{
//桌子上有包了 就唤醒别人,自己等待
notifyAll();
wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//吃包子
public synchronized void getbao(){
try {
String name=Thread.currentThread().getName();
//如果桌子上有包子就直接吃
if(list.size()!=0){
list.clear(); //吃空桌上的食物
Thread.sleep(2000); //吃东西要两秒
System.out.println(name+"吃了一个肉包子");
//吃完了就唤醒其他人,自己等待
notifyAll();
wait();
}
else{
//如果没有包子直接等待
notifyAll();
wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试类:
public class demo1 {
public static void main(String[] args) {
//生产消费模型
Desk desk=new Desk();
//三个厨师,两个吃货 ,一共五个线程
new Thread(()->{
while (true){
desk.put();
}
},"厨师一号").start();
new Thread(()->{
while (true){
desk.put();
}
},"厨师二号").start();
new Thread(()->{
while (true){
desk.put();
}
},"厨师三号").start();
new Thread(()->{
while (true){
desk.getbao();
}
},"吃货一号").start();
new Thread(()->{
while (true){
desk.getbao();
}
},"吃货二号").start();
}
}
执行结果:
厨师一号做了一个肉包子!! 吃货二号吃了一个肉包子 厨师三号做了一个肉包子!! 吃货一号吃了一个肉包子 厨师一号做了一个肉包子!! 吃货一号吃了一个肉包子 厨师三号做了一个肉包子!!
..............
这些线程就会一直做一个吃一个做一个吃一个......
线程池
什么是线程池
线程池就是一个可以复用线程的技术。
那么为什么要使用线程池呢?如果不使用线程池用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能
线程池的工作原理
使用线程池就不会来一个任务就创建一个线程,而是原先定义的几个线程循环的使用的。可以减少内存的开销
如何创建线程池
JDK5.0起提供了代表线程池的接口:ExecutorService
如何的得到线程池对象?
方式一: 使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
ThreadPoolExecutor构造器
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
参数一:corepoolSize:指定线程池的核心线程的数量
参数二:maximumPoolSize:指定线程池的最大线程数量
参数三:keepAliveTime:指定临时线程的存活时间
参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
参数五:workQueue:指定线程池的任务队列
参数六:threadFactory:指定线程池的线程工厂
参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)
可以把构造器想象成为一个餐馆:
参数一:正式工3
参数二:最大员工数量:5 临时工就为:2
参数三:临时工空闲多久被开除
参数四:临时工空闲时间的单位
参数五:客人排队的地方
参数六:负责招聘员工的(hr)
参数七:忙不过来了怎么处理新来的客人?
ExecutorService pool=new ThreadPoolExecutor(3,5,
8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
方式二: 使用Executors(线程池的工具类) 调用方法返回不同特点的线程池对象
**Executors**是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。
| 方法名称 | 说明 |
|---|---|
| public static ExecutorService newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池,如果某个线程因为执行异e而结束,那么线程池会补充一个新线程替代它 |
| public static Executorservice newsingleThreadExecutor() | 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。 |
| public static ExecutorService newCachedThreadPool() | 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了6es则会被回收掉。 |
| public static scheduledExecutorService newScheduledThreadPool(int corePoolsize) | 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。 |
注意∶这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。
//通过Executors创建一个线程池对象
ExecutorService pool=Executors.newFixedThreadPool(3);
//注意该出ScheduledExecutorService继承了ExecutorService。
ScheduledExecutorService pool1=Executors.newScheduledThreadPool(3);
//延迟执行任务
pool1.schedule(new MyRunnable(),10,TimeUnit.SECONDS);
//定时执行任务,没隔多长时间久执行一次
//第一次打印出来是用来2s中,以后开始每5s打印一次。
pool1.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("定时任务执行了!!!");
}
},2,5,TimeUnit.SECONDS);
线程池的注意事项:
-
临时线程什么时候创建?
- 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
-
什么时候会开始拒绝新任务?
- 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务
线程池处理Runnable 任务
ExecutorService的常用方法
| 方法名称 | 说明 |
|---|---|
| void execute( Runnable command) | 执行Runnable 任务 |
| Future submit(Callable task) | 执行callable 任务,返回未来任务对象,用于获取线程返回的结果 |
| void shutdown() | 等全部任务执行完毕后,再关闭线程池! |
| List shutdownNow() | 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务 |
public class createThread11 {
public static void main(String[] args) {
//创建线程池的第一种方式
//创建一个线程池对象
ExecutorService pool=new ThreadPoolExecutor(3,5,
8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
//使用线程池处理runnable任务
//创建runnable任务对象
Runnable target=new MyRunnable();
pool.execute(target); //线程池会自动创建一个新的线程,自动处理这个任务,自动执行的
pool.execute(target); //线程池会自动创建一个新的线程,自动处理这个任务,自动执行的
pool.execute(target); //线程池会自动创建一个新的线程,自动处理这个任务,自动执行的
pool.execute(target); //等待前面3个线程执行完了,有空了就找我,
pool.execute(target); //等待前面3个线程执行完了,有空了就找我
pool.execute(target); //等待前面3个线程执行完了,有空了就找我
pool.execute(target); //等待前面3个线程执行完了,有空了就找我,任务队列满了
//如果现在还有任务需要进入到任务队列中,那么久需要创键临时对象了
pool.execute(target);
pool.execute(target);
//所有线程都在忙了,任务队列也满了,就到了拒绝新任务的时机
pool.execute(target);
//等待所有任务执行完成,就关闭线程池,不然程序会一直执行的
pool.shutdown();
}
}
可能不太理解创建线程池的最后一个参数怎么写?
下面是新任务拒绝策略
| 策略 | 详解 |
|---|---|
| ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常。是默认的策略 |
| ThreadPoolExecutor.DiscardPolicy: | 丢弃任务,但是不抛出异常这是不推荐的做法 |
| ThreadPoolExecutor.Discard0ldestPolicy | 抛弃队列中等待最久的任务然后把当前任务加入队列中 |
| ThreadPoolExecutor.CallerRunsPolicy | 由主线程负责调用任务的run()方法从而绕过线程池直接执行 |
使用线程池处理Callable 任务
主要用 Future<T> submit(Callable<T> tesk)
案例:计算1-n的和
public class demo22 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建线程池的第一种方式
//创建一个线程池对象
ExecutorService pool=new ThreadPoolExecutor(3,5,
8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
//使用线程池处理callable 任务
Future<String> fu1=pool.submit(new myCalls(100));
Future<String> fu2=pool.submit(new myCalls(200));
Future<String> fu3=pool.submit(new myCalls(300));
Future<String> fu4=pool.submit(new myCalls(400));
//获取任务对象返回的值
System.out.println(fu1.get()); //这是线程锁计算出来的数:5050
System.out.println(fu2.get()); //这是线程锁计算出来的数:20100
System.out.println(fu3.get()); //这是线程锁计算出来的数:45150
System.out.println(fu4.get()); //这是线程锁计算出来的数:80200
pool.shutdown(); //任务执行完后,关闭线程池
}
}
//callable 任务类 计算1- n 的和
public class myCalls implements Callable<String> {
private int n;
public myCalls(int n) {
this.n=n;
}
@Override
public String call() throws Exception {
String name1=Thread.currentThread().getName();
System.out.println(name1);
int sum=0;
for (int i = 0; i <= n; i++) {
sum+=i;
}
return "这是线程锁计算出来的数:"+sum;
}
}
核心线程数量如何配置
计算密集型的任务:核心线程数量=CPU的核数+1 (比如上面的例子就是计算密集型任务,我的计算机CPU核数为16,所以我最好是设计核心线程数量为17)
IO密集型的任务:核心线程数量=CPU核数*2
注意事项executors
线程池最好不要使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下︰
FixedThreadPool和SingleThreadPool :允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM;CachedThreadPool :允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM(内存用完了)
并发、并行
首先要知道什么是进程、线程?
正在运行的程序(软件)就是一个独立的进程。
线程是属于进程的,一个进程中可以同时运行很多个线程。
进程中的多个线程其实是并发和并行执行的
并发的含义
进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
并行的理解
在同一时刻,同时有多个线程在被CPU调度
所以多线程是并发和并行同时进行的
线程的生命周期
人类的生命周期:
婴儿----- 单身-----婚后-----死去;大概是这样从生到死,而且在其中也会有一些状态的改变,比如婚后也可能又变回单身等。
那么线程的生命周期也就是线程从生到死的过程中,经历的各种状态及状态转换。
理解线程这些状态有利于提升并发编程的理解能力。
Java线程的状态:Java总共定义了6种状态,6种状态都定义在Thread类的内部枚举类中。
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
| 线程状态 | 说明 |
|---|---|
| NEW(新建) | 线程刚被创建,但是并未启动。 |
| Runnable(可运行) | 线程已经调用了start(),等待CPU调度 |
| Blocked(锁阻塞) | 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态;。 |
| waiting(无限等待) | 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒 |
| Timed waiting(计时等待) | 同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed waiting状态。 |
| Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
悲观锁和乐观锁
悲观锁:一上来就加锁,没有安全感。每次只能一个线程进入访问完毕后,再解锁。线程安全,性能较差!
乐观锁:一开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全问题的时候才开始控制。线程安全,性能较好。
就比如:一个公共厕所,有很多人去上厕所,一个人进入厕所感觉没有安全感关上门(上锁),别人就进不来了,这就是悲观锁;然而如果这个人不关门,其他的人是可以进来的就是看着你上厕所,但是真正上厕所的(处理资源)的只有一个人(乐观锁)
乐观锁,就是每次拿数据的时候都假设为别人不会修改,所以不会上锁;只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新了,要么报错,要么自动重试。