多线程【重点】
一、基础知识
1、进程和线程区别
进程是资源分配的最小单位,线程是cpu调度的最小单位,一个进程中可以包含多个线程
2、线程的三种创建方式
-
继承Thread类
package com.sqx; /* * 创建线程方式一 */ public class CreatThread { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); //开辟新的占空间,该方法执行完成后瞬间结束! for(int i = 0 ; i < 100 ; i++ ){ System.out.println("当前执行main线程----->" + i); } } } class MyThread extends Thread{ @Override public void run() { try { Thread.sleep(5000); //当前线程休眠5秒 } catch (InterruptedException e) { e.printStackTrace(); } for(int i = 0 ; i < 100 ; i ++ ) { System.out.println("当前执行分支线程------>" + i); } } } /* 输出结果: 当前执行main线程----->62 当前执行分支线程------>0 当前执行分支线程------>1 当前执行分支线程------>2 当前执行main线程----->63 */
-
实现Runnable接口
/* * 创建线程方式二 【常用一些】 */ public class CreatThread2 { public static void main(String[] args) { /* MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable);*/ //格式2 //Thread thread = new Thread(new MyRunnable()); //格式1 Thread thread = new Thread(new Runnable() { //匿名内部类 public void run() { for(int i = 0 ; i < 100 ; i ++ ) { System.out.println("当前执行分支线程------>" + i); } Thread thread = Thread.currentThread(); //静态方法!获取当前线程对象 System.out.println("当前线程"+thread); //得到当前线程对象,当前线程Thread[Thread-0,5,main] } }); thread.start(); //开辟新的占空间,该方法执行完成后瞬间结束! for(int i = 0 ; i < 100 ; i++ ){ System.out.println("当前执行main线程----->" + i); } } } class MyRunnable implements Runnable{ public void run() { for(int i = 0 ; i < 100 ; i ++ ) { System.out.println("当前执行分支线程------>" + i); } } } /* 测试结果: 当前执行main线程----->84 当前执行分支线程------>33 当前执行main线程----->85 当前执行分支线程------>34 当前执行main线程----->86 * */
-
实现Callable接口 (jDK8新特性)
//FutureTask方式,实现Callable接口 ,优点:该线程可以有返回值! public class CreatThread3 { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask task = new FutureTask(new Callable() { public Object call() throws Exception { System.out.println("分支线程开始!"); return 123; } }); Thread thread = new Thread(task); thread.start(); System.out.println(task.get()); //主线程获取分支线程的返回值 } }
3、线程的生命周期
线程有时间片,才会运行,不然就抢夺时间片!
4、获取当前线程对象
Thread thread = Thread.currentThread(); //静态方法!获取当前线程对象
thread.getName() ; thread.setName() ;
5、线程休眠 Thread.sleep
package com.sqx;
/*
* 线程休眠,中断线程线程休眠
* */
public class ThreadSleep {
public static void main(String[] args) {
MyRunnable03 myRunnable03 = new MyRunnable03();
Thread t = new Thread(myRunnable03);
t.start();
System.out.println("main线程开始");
try {
Thread.sleep(1000*5); //main线程休眠5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// t.interrupt(); //方式一 : 中断分支线程,抛出异常sleep interrupted
// t.stop(); //直接杀死线程!栈都回收了,造成丢失数据!(该方法已经过时)
// 合理终止线程休眠(常用): 打个bool 标价,如果true就继续休眠,否则直接结束
myRunnable03.isSleep = false ;
}
}
class MyRunnable03 implements Runnable{
boolean isSleep = true ;
public void run() {
for( int i = 0 ; i < 10 ; i ++ ) {
if (isSleep == true){
System.out.println("分支线程 ---- >" + i);
try {
Thread.sleep(1000*1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
System.out.println("分支线程 ---- >" + i);
return ;
}
}
}
}
//线程休眠会使得当前线程放弃之前抢夺的时间片,从而线程进入就绪状态!
了解
-
线程优先级
public class ThreadPriority { public static void main(String[] args) { System.out.println(Thread.MAX_PRIORITY); //线程的最大优先级 10 System.out.println(Thread.MIN_PRIORITY); //线程的最小优先级 0 System.out.println(Thread.NORM_PRIORITY); //线程的默认优先级 5 System.out.println(Thread.currentThread().getPriority()); //获取当前线程的优先级 Thread.currentThread().setPriority(10); //设置当前线程的优先级为10 } }
-
线程合并
Thread thread = new Thread(new MyRunnable05()); thread.start(); thread.join(); //讲thread线程合并到当前main线程中执行,栈不会改变仅仅是执行顺序!
-
线程让位
Thread.yield(); //当前线程让一下,当前线程此刻不回去抢占时间片,下一刻继续抢占
需要注意:Java的线程调度是抢占式调度模型,优先级高的线程抢占时间片的概率大!
二、线程安全 *
线程安全问题的产生需要满足:多线程并发、共享数据、共享数据有修改操作 ,如:两人同时取银行卡里的余额!
怎么解决线程安全问题?
- 引入“线程同步机制”,也就是让线程排队,不再并发执行
- 为了安全,可以牺牲一部分效率
了解 异步,同步编程模型
- 异步编程模型 : 线程t1 ,t2各自执行各自的,t1不管t2,t2不管t1 ;谁也不需要等谁,本质就是多线程并发
- 同步编程模型: 线程t1 ,t2需要满足 t2执行之前必须等待t1线程执行结束,两线程发生了等待关系 本质:线程排队执行
注意:异步就是并发,同步就是排队
Synchronized 同步代码块
//t1 、t2 两个线程同时向一个共享账户Account发起取款,如何避免线程的并发执行 ?
在我们的取款方法体上添加一层
public void withdraw(int count){
synchronized (需要排队的线程的共享对象){ //注:一个对象只有一把锁
方法体
}
}
当我们的t1 、t2线程调用withdraw方法时,先遇到synchronized(共享对象)的线程,会直接拿走锁池(lock)中共享对象的对象锁,从而执行方法体,此时共享对象的锁已经被占用,我们的另外一个线程只能等待共享对象的锁的归还,也就是等待上一个线程线程执行结束该同步代码块,执行结束才会归还锁,我们未执行该方法的线程再去获取锁,去执行方法!
为什么非得传入共享对象?
因为传入一个共享对象,两个线程中共享的这一个对象的对象锁被拿走,另一个对象无法获取,因此无法进入同步代码块!但是如果传入的是非共享对象,则一个线程把这个非共享对象的锁拿走,并不影响另外一个线程任何操作! (共享可以看作,两线程共用这一个)
局部变量是线程安全的!因为存储在栈中,数据不共享,常量不可修改也是线程安全的!
静态变量和实例变量则是分别在方法区和队中,存在数据共享,可能会导致线程不安全问题
Synchrnoized三种写法
方式一:同步代码块
public void withdraw(int count){
synchronized (需要排队的线程的共享对象){ //优点:灵活,哪里需要加哪里
方法体
}
}
方式二:实例方法上添加Synchronized
public synchronized void withdraw(int count){ //默认是方法体中的所有都需要同步,而且共享对象为this
//优点:省代码
}
方式三:在类上添加Synchronized
//表示找类锁,一个类一个锁,即使创建100个对象也仍然只有1个锁!
扩展:对象锁保证的是实例变量的安全、类锁保证的是静态变量的安全!
三、死锁
实现一个死锁,由于死锁不会报错,因此很难调试,我们只有会写死锁,以后才能注意 !
package com.sqx;
/*
* 实现一个死锁!
* */
public class DeadLock {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
MyThread1 thread1 = new MyThread1(o1, o2);
MyThread2 thread2 = new MyThread2(o1, o2);
thread1.start();
thread2.start();
}
}
class MyThread1 extends Thread{
Object o1 ;
Object o2 ;
public MyThread1(Object o1 , Object o2){
this.o1 = o1 ;
this.o2 = o2 ;
}
public void run(){
synchronized (o1){
synchronized (o2){ //等待o2锁的释放
}
}
}
}
class MyThread2 extends Thread{
Object o1 ;
Object o2 ;
public MyThread2(Object o1, Object o2){
this.o1 = this.o1;
this.o2 = this.o2;
}
public void run(){
synchronized (o2){ //等待o1锁的释放
synchronized (o1){
}
}
}
}
四、守护线程
守护线程一般在默默运行,用户线程全部结束,守护线程也会自动结束!
线程分为两类:
- 用户线程
- 守护线程
创建一个线程,直接设置为守护线程即可
t.setDeamon(true) //讲当前t线程设置为守护线程!
五、定时器
实际开发中,每隔多久执行一段特定的程序,这种需求是很常见的!
方式一:Thread.Sleep, 设置睡眠多长时间,执行任务,最原始的方式 ;
方法二:Java类库中写好的一个定时器java.util.Timer,可以直接用,但是用的少,框架一般都带自己的定时器,
方式三 : 使用最多的就是spring框架当中提供的springTask框架,只需简单的配置就可以完成定时器(底层还是方式二);
//方式二的实现原理
public class TimeTest {
public static void main(String[] args) throws ParseException {
Timer timer = new Timer(); //创建定时对象
//Timer timer = new Timer(true); Timer以守护线程的形式运行
//指定定时任务(要执行的任务,第一次执行时间,间隔多久执行一次)
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date FirstDate = dateFormat.parse("2021-9-28 17:40:00");
timer.schedule(new LogTask(),FirstDate,1000*10); //可以改为匿名内部类方式
}
}
class LogTask extends TimerTask{
public void run() {
//定时执行的任务
System.out.println("到时间了!");
}
}
六、wait和notify
wait() 和 notify () 是Object 的方法 , 通常结合synchrnoized使用
- wait()意思是说,我等会儿再用这把锁(对象锁!),CPU也让给你们,我先休息一会儿!
- notify()意思是说,我用完了,你们谁用?
测试代码:
package com.sqx;
public class WaitTest {
public static void main(String[] args) {
Object obj = new Object() ;
MyThread01 t1 = new MyThread01(obj);
MyThread02 t2 = new MyThread02(obj);
t1.start();
t2.start();
}
}
class MyThread01 extends Thread{
Object object ;
public MyThread01(Object object) {
this.object = object ;
}
@Override
public void run() {
synchronized (object){
System.out.println("T1线程执行");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T1线程结束!");
}
}
}
class MyThread02 extends Thread{
Object object ;
public MyThread02(Object object) {
this.object = object ;
}
@Override
public void run() {
synchronized (object){
System.out.println("T2线程执行");
object.notify();
System.out.println("T2线程结束!");
}
}
}
/*
结果:
T1线程执行
T2线程执行
T2线程结束!
T1线程结束!
流程:
T1启动,让出锁,让出CPU,T2获得CPU,启动,唤醒使用了object的休眠的线程,
T1被唤醒后等待启动,T2继续执行,T2执行完,T1获得CPU后继续执行。
* */
生产者消费者模式
多线程基础了解到这里即可,我们接下来就是JUC