多线程使用、同步锁、源码分析
文章目录
- 2.1 继承Thread类创建线程类
- 2.2 实现Runnable接口
- 2.3 通过Callable和Future创建线程
- 1.4 方式对比
- 1.4.1 实现接口 VS 继承 Thread
- 1.4.2 Runnable接口和Callable接口的区别
1. 线程的生命周期
(1)一个java应用程序至少有两个线程,一个是主线程(main),一个是执行垃圾回收的线程。
(2)生命周期可参见CSDN博客:https://www.geek-share.com/detail/2690198901.html
1.1 新建
新建线程:在内存中开启一片资源,以存储该线程对象。
1.2 执行start()后,进入队列等待获取CPU时间片
1、涉及到:JVM的线程调度器——用于把线程优先级Priority划分为10个级别;监视和控制Runnable状态的线程。线程优先级范围是[1,10],默认为5。
2、 高优先级的队列之间轮流执行完毕run( )方法,死亡之后,才轮流执行低优先级队列的线程。
1.3 开始运行run()方法
线程脱离主线程(因为JVM把 CPU使用权/时间片 切换给了该线程)。
1.4 中断
中断:cpu资源切换到其他线程。有如下几种中断情况:
(1)执行sleep(int ms),当前线程睡眠,让出cpu使用权,但不释放同步锁。经过ms毫秒后,线程主动进入到“线程排队等待队列”中。
(2)执行wait(),当前线程 挂起 / 等待,让出cpu使用权,释放同步锁。不主动进入到“线程排队等待队列”中,需要其他线程调用notify()通知该线程才能进入到“线程排队等待队列”中。注意:notify(),notifyAll(),wait()方法必须在同步代码中才能使用。
(3)读/写阻塞,此时线程不进入“线程排队等待队列”中,只有消除阻塞时才能进入到“线程排队等待队列”中。
1.5 死亡
线程死亡:线程实体内存被释放,死亡的线程需要重新new实体后才能start( )。死亡原因有如下几种:
(1)正常运行完run( )
(2)强制结束了run( )
1.6 注意事项:
(1)子线程一旦启动,其地位和主线程是一样的,所以一旦主线程结束了,子线程不会受影响,不会跟着结束。
(2)线程没有结束run( )之前,不要让该线程再调用start( )方法,否则发生IllegalThreadStateException异常
2. 创建线程的三种方式
Java创建线程的三种方式 |
---|
1、继承Thread类创建线程类 |
2、实现Runnable接口(推荐使用) |
3、通过Callable和Future创建线程 |
2.1 继承Thread类创建线程类
步骤:
(1)定义Thread类的子类,重写父类的run()方法;
(2)创建子类的实例;
(3)启动线程:调用start()方法。
public class SubThread extends Thread { private int ticket = 10; private String name; public SubThread(String name) { this.name = name; } public static void main (String []args){ SubThread t1 = new SubThread("A"); SubThread t2 = new SubThread("B"); t1.start(); t2.start(); } public void run() { while (ticket > 0){ System.out.println("" + ticket + " is saled by " + name); ticket--; try{ //睡眠时间:[0,9]之间的随机整数,单位ms sleep((int)Math.random() * 10); }catch (InterruptedException e){ e.printStackTrace(); } } } }
2.2 实现Runnable接口
步骤:
(1)定义实现Runnable接口的实现类,重写run()方法;
(2)创建Runnable接口实现类的实例,并将该实例作为参数传到Thread类的构造方法中来创建Thread对象,该Thread对象才是真正的线程对象;
(3)启动线程:调用start()方法。
例子1:两个线程分别启用不同的 Runnable接口实现类的实例
不共享ticket = 10
public class ImplThread implements Runnable { private int ticket = 10; private String name; public ImplThread(String name) { this.name = name; } public static void main (String []args){ //两个线程各自使用不同的Runnable实现对象,互不干扰 ImplThread i1 = new ImplThread("A"); ImplThread i2 = new ImplThread("B"); Thread t1 = new Thread(i1); Thread t2 = new Thread(i2); t1.start(); t2.start(); } public void run() { while (ticket > 0){ System.out.println("" + ticket + " is saled by " + name); ticket--; try{ //睡眠时间:[0,9]之间的随机整数,单位ms sleep((int)Math.random() * 10); }catch (InterruptedException e){ e.printStackTrace(); } } } }
例子2:两个线程分别启用同一个 Runnable接口实现类的实例
共享ticket = 10
public class ImplThread implements Runnable { private int ticket = 10; private String name; public ImplThread(String name) { this.name = name; } public static void main (String []args){ //两个线程共享同一个Runnable实现对象 ImplThread i = new ImplThread(); Thread t1 = new Thread(i); Thread t2 = new Thread(i); t1.start(); t2.start(); } public void run() { while (ticket > 0){ System.out.println(ticket-- + "is saled by" + Thread.currentThread()); ticket--; try{ //睡眠时间:[0,9]之间的随机整数,单位ms sleep((int)Math.random() * 10); }catch (InterruptedException e){ e.printStackTrace(); } } } }
2.3 通过Callable和Future创建线程
与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装
package TreadLearning; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Call { public static void main(String[] args) throws InterruptedException, ExecutionException { //创建一个可重用固定线程数量的线程池服务 ExecutorService ser =Executors.newFixedThreadPool(1); //创建线程 ThreadImpCallable demo = new ThreadImpCallable(); //通过线程池使线程执行,并通过Future<Integer>的get( )方法得到返回值num Future<Integer> result = ser.submit(demo); int num = result.get(); //打印 System.out.println(num); //停止服务 ser.shutdownNow(); } } class ThreadImpCallable implements Callable<Integer>{ @Override public Integer call() throws Exception { return 1000; } }
1.4 方式对比
1.4.1 实现接口 VS 继承 Thread
实现接口会更好一些。原因如下:
1、因为Java 不支持多重继承,继承了 Thread 类就无法继承其它类。
2、Java可以实现多个接口。
3、类可能只要求可执行就行,继承整个 Thread 类开销过大。
1.4.2 Runnable接口和Callable接口的区别
有点深的问题了,也看出一个Java程序员学习知识的广度。
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。
而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。
3. 创建线程池
使用线程池并不是创建线程,而是对线程进行管理。Excetor为线程池超级接口,该接口中定义了一个execute(Runnable command)方法,用来执行传递过来的线程,ExecutorService就是我们所说的线程池,它继承了Excetor接口。如何创建线程池呢?Java提供了Executors类,该类有四个静态方法分别可以创建不同类型的线程池(ExecutorService)。
Executors.newCachedThreadPool() 创建可变大小的线程池
Executors.newFixedThreadPool(int number) 创建固定大小的线程池
Executors.newSingleThreadPool() 创建单任务线程池
Executors.newScheduledThreadPool(int number) 创建延迟线程池
import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; public class Test { public static void main(String[] args){ //创建一个可重用固定线程数量的线程池 ExecutorService pool = Executors.newFixedThreadPool(2); //创建Thread类的子类的线程实例 Thread t1 = new MyThread(); Thread t2 = new MyThread(); Thread t3 = new MyThread(); Thread t4 = new MyThread(); Thread t5 = new MyThread(); //将线程放入池中开始执行,请求cpu时间片 pool.execute(t1); pool.execute(t2); pool.execute(t3); pool.execute(t4); pool.execute(t5); //关闭线程池 pool.shutdown(); } } class MyThread extends Thread{ @Override public void run() { System.out.println(Thread.currentThread().getName()+"线程 正在执行。。。"); } }
运行结果如下,可见线程得到了重用,线程池里只有两个线程在执行。
pool-1-thread-1正在执行。。。 pool-1-thread-1正在执行。。。 pool-1-thread-1正在执行。。。 pool-1-thread-1正在执行。。。 pool-1-thread-2正在执行。。。 Process finished with exit code 0
4. 线程同步----synchronized关键字
4.1 线程?
一份独立运行的程序,有自己专用的运行空间,可以和其他线程共享一些资源,比如,内存,文件,数据库等。
4.2 线程同步?
指多个线程一起进行,共同占用一个资源。线程同步的“同”真正解释是指多个线程协同、协助、互相配合地使用同一资源。比如,一个厕所同一时刻只能给一个人占用,当一个人进去的时候,其他人就要在外面等待,里面的人出来以后外面的人才能够进去使用。
4.3 为什么需要线程同步?
最好的例子——银行的账户,假设你在银行开了一个账户,Account,同时拿到一本存折还有一张银行卡,假设你的银行卡里面有3000元。有一天,你和你的女朋友分别拿着银行卡和存折同时在ATM取款机还有银行柜台取钱。两种方式两个线程,共享Account这个账户。假使你用银行卡在ATM机取钱(线程1)时,线程1在操作Account,取出3000,但是在ATM吐出钱而Account还没有扣钱的时候,线程2又启动了,因为线程2并不知道线程1已经取了3000,你的女朋友在柜台又取了3000。最终银行会亏了3000。
4.4 synchronized使用在不同位置的区别?
synchronized可以锁住:代码块,实例方法,类方法。
- synchronized用在方法上?此时,锁住的是当前对象的当前方法,会使得其他线程访问该对象的synchronized方法或者代码块时 阻塞,但并不会阻塞非synchronized方法。
- synchronized(this){…代码块…} 锁住的什么?此时,锁住的是当前的对象this。当synchronized块里的内容执行完之后,释放当前对象的锁。同一时刻若有多个线程访问这个对象,则会被阻塞。
- synchronized(object){…代码块…} 锁住的什么?锁住的是object对象。当synchronized块里的内容执行完之后,释放object对象的锁。同一时刻若有多个线程访问这个对象,则会被阻塞。注意:如果object为Integer、String等等包装类时(new出的对象除外),并不会锁住当前对象,也不会阻塞线程。因为包装类是final的,不可修改的,如果修改则会生成一个新的对象。所以,在一个线程对其进行修改后,其他线程在获取该对象的锁时,该对象已经不是原来的那个对象,所以获取到的是另一个对象的锁,所以不会产生阻塞。
- synchronized(Object.class){…代码块…} 锁住什么?锁住Object类,即Object的所有对象
- 总结
4.5 银行例子----synchronized锁住某方法
synchronized锁住某对象的某方法后,
- 主函数所在类MainClass,启动
/**功能:会计员和出纳员,拥有同一个账本,他俩同时可以对账本进行访问,为银行打入收到的钱。这里使用synchronized同步机制防止两个线程同时访问账本。 *(1)、会计员线程在账本上存300元的过程:分三次存入,每次存入100元,每存入一次都休息1000毫秒。 *(2)、出纳员线程在账本上取150元的过程:分三次取出,每次取出50元, 每存入一次都休息2000毫秒。 * 注意:当其中一个人操作账本时,不允许另一人使用账本。 * **/ package 线程同步; public class MainClass { public MainClass() { } /** 主函数 * @author 黄军威 * @date 2019/8/3 10:53 */ public static void main(String[] args) { //创建共享对象资源:实现了Runnable接口的类 BankRunnable bankRunnable = new BankRunnable(); bankRunnable.moneyset(0); //启动两个线程,操作同一个资源 bankRunnable.accountant.start(); bankRunnable.cashier.start(); } }
- 银行类Bank,用synchronized锁住addMoney(…)方法
package 线程同步; public class BankRunnable implements Runnable{ int money; //该银行总共拥有的钱 Thread accountant; //会计员 Thread cashier; //出纳员 public BankRunnable() { //创建两个线程,共享资源为当前对象 accountant = new Thread(this); cashier = new Thread(this); } /**设置钱的数量*/ public void moneyset(int moneyIn){ money = moneyIn; } /** run(),收钱方法addMoney(int amount)采用同步机制*/ public void run(){ Thread t = Thread.currentThread(); if(t == accountant){ accountant = t; t.setName("会计"); addMoney(300); } if(t == cashier){ cashier = t; t.setName("出纳"); addMoney(150); } System.out.println(t.getName()+"线程执行已结束!\n"); } /**为银行收钱 的方法,方法要求会计、出纳两个线程满足同步机制*/ private synchronized void addMoney(int amount){ //会计员 休息式 收钱:分3次收钱,每次收用户的 1/3,然后sleep(...),但是不释放同步锁,即addMoney方法不允许其他线程执行,只能等待当前线程执行完addMoney方法 if(Thread.currentThread() == accountant){ for(int i = 1; i<=3; i++){ money = money + amount/3; System.out.print("("+ i + "). " + Thread.currentThread().getName() + "目前收了" + amount*i/3 + "元。........"); try{ Thread.sleep(1000);//让出CPU使用权至少x000毫秒,但是不释放同步锁 System.out.print("开始沉睡" + i + "........"); } catch(InterruptedException e){ System.out.println(Thread.currentThread().getName() + "开始沉睡2000毫秒\n"); if(Thread.currentThread().isInterrupted() == true){ System.out.println(Thread.currentThread().getName() + "正在沉睡"); } else{ System.out.println(Thread.currentThread().getName() + "提前结束沉睡"); } } System.out.println(Thread.currentThread().getName() + "结束沉睡" + i); } } //出纳员 休息式 收钱 if(Thread.currentThread() == cashier){ for(int i = 1; i<=3; i++){ money = money - amount/3; System.out.print("(" + i + ")." + Thread.currentThread().getName() + "目前收了" + amount*i/3 + "元。........"); try{ Thread.sleep(2000);//让出CPU使用权至少x000毫秒,但是不释放同步锁 System.out.print("开始沉睡" + i + "........"); } catch(InterruptedException e){ System.out.println("开始沉睡2000毫秒\n"); if(Thread.currentThread().isInterrupted() == true){ System.out.println(Thread.currentThread().getName() + "正在沉睡"); } else{ System.out.println(Thread.currentThread().getName() + "提前结束沉睡"); } } System.out.println(Thread.currentThread().getName() + "结束沉睡" + i); } } System.out.println("收钱成功!目前银行内余额为:" + money + "元。"); } }
- 执行结果
(1). 会计目前收了100元。........开始沉睡1........会计结束沉睡1 (2). 会计目前收了200元。........开始沉睡2........会计结束沉睡2 (3). 会计目前收了300元。........开始沉睡3........会计结束沉睡3 收钱成功!目前银行内余额为:300元。 会计线程执行已结束! (1).出纳目前收了50元。........开始沉睡1........出纳结束沉睡1 (2).出纳目前收了100元。........开始沉睡2........出纳结束沉睡2 (3).出纳目前收了150元。........开始沉睡3........出纳结束沉睡3 收钱成功!目前银行内余额为:150元。 出纳线程执行已结束!
5. synchronized底层实现
synchronized底层实现可以参考博客:https://www.jianshu.com/p/d53bf830fa09
6.
- Android进阶——多线程系列之异步任务AsyncTask的使用与源码分析
- 锁对象-Lock: 同步问题更完美的处理方式 (ReentrantReadWriteLock读写锁的使用/源码分析)
- 第二人生的源码分析(二十五)人物行走与服务器同步
- .NET / Rotor源码分析5 - 开始使用WinDbg+SOS调试,sscoree.dll,加载SOS并设置JIT断点
- extjs源码分析-Ext.util.TaskRunner(模拟多线程)
- 第二人生的源码分析(二十五)人物行走与服务器同步
- 蔡军生先生第二人生的源码分析(五十八)使用FreeType字体
- curl源码分析(一)webkit中curl库的使用
- 如何使用androidpn实现android手机消息推送(简单的源码分析)
- .NET / Rotor源码分析5 - 开始使用WinDbg+SOS调试,sscoree.dll,加载SOS并设置JIT断点
- 第二人生的源码分析(六十六)使用Expat XML解析器的例子
- 蔡军生先生第二人生的源码分析(六十九)使用LLXmlTree类来分析XML配置文件
- Lighttpd1.4.20源码分析之fdevent系统(3) -----使用
- curl源码分析(二)协议注册与使用过程
- AsyncTask的源码分析 与 使用 之我的浅解
- tabhost简单使用及tabhost源码分析
- leveldb源码分析 之 入门使用
- .NET / Rotor源码分析5 - 开始使用WinDbg+SOS调试,sscoree.dll,加载SOS并设置JIT断点
- 第二人生的源码分析(四十一)使用Apache运行库线程
- 第二人生的源码分析(六十七)LLXMLNode使用Expat库打开文件