1-进程和线程
1.1 什么是进程
- 程序是静止的,运行中的程序就是进程,是系统的进行资源分配和调用的独立单位。
- 每一个进程都有它自己的内存空间和系统资源。
1.1.1 进程的特征
1)动态性: 进程是运行中的程序,要动态的占用内存,CPU和网络等资源。
2)独立性 : 进程与进程之间是相关独立的,彼此有自己的独立内存区域。
3)并发和并行
并行: 某个时间段同时运行多个程序。
并发: 在某个时间点同时运行多个程序。
1.2 什么是线程
- 线程是属于进程的。一个进程可以包含多个线程,这就是多线程。
- 在同一个进程内可以执行多个任务,而这每一个任务就可以看成是一个线程。
- 线程是程序的执行单位,执行路径是程序使用cpu的最基本单位。线程也支持并发性。
1.2.1 线程的作用
- 可以提高程序的效率,可以有更多机会得到CPU。多线程可以解决很多业务模型。
- 大型高并发技术的核心技术,设计到多线程的开发可能都比较难理解。
1.2.2 进程与线程的区别
- 线程是在进程的基础上划分的。
- 线程消失了,进程不会消失,进程如果消失了,则线程肯定消失。
2- 线程创建
2.1 继承Thread类
2.1.1Thread类基本概念
- java.lang.Thread类代表线程,任何线程对象都是Thread类(子类)的实例。
- Thread类是线程的模板,封装了复杂的线程开启等操作,封装了操作系统的差异性。
定义一个线程类继承Thread类,然后重写run()方法,再创建线程对象,调用start()方法启动线程。
2.1.2 代码示例
package cn.guardwhy_01;
/**
方式一的步骤:
a.定义一个线程类继承Thread类
b.重写Thread类的run()方法
c.创建一个子线程对象
d.调用线程对象的start()方法启动线程(其实最终就是执行线程对象的run()方法)
线程的注意:
1.启动线程不能直接调用run()方法,否则是普通对象的普通方法调用了,将失去线程特征。线程的启动必须调用start()
2.一般是先创建子线程,再申明主线程的任务,否则主线程任务总是先执行完!
优缺点:
优点:编码简单。
缺点:线程类已经继承了Thread类,不能再继承其他类,功能被削弱了。不能做线程池。无法直接返回线程执行的结果。
*/
// a.定义一个线程类继承Thread类
class MyThread extends Thread{
// 重写Thread类的run()方法
@Override
public void run() {
for(int i=0; i < 10; i++){
System.out.println("子线程执行:" + i);
}
}
}
public class ThreadDemo02 {
public static void main(String[] args) {
// 创建一个子线程对象
MyThread t = new MyThread();
// 启动线程,线程的启动必须调用start()
t.start();
// 遍历操作
for(int i=0; i<10; i++){
System.out.println("main线程执行:" + i);
}
}
}
2.2 Runnable接口
2.2.1 Runnable基本概念
定义一个线程任务类实现Runnable接口,然后重写run()方法,创建线程任务对象,把线程任务对象包装成线程对象,调用start()方法启动线程。
2.2.2 代码示例(普通方式)
package cn.guardwhy_03;
/**
方式二:
a.定义一个线程任务类实现Runnable接口。重写run()方法
b.创建一个线程任务对象
c.把线程任务对象包装成一个线程对象
-- public Thread(Runnable target)
d.调用线程对象的start()方法启动线程。
优缺点:
缺点:编程相对复杂,不能直接返回线程的执行结果
优点:
1. 一个任务对象可以被反复包装成多个线程对象。
2. 可以避免java中的单继承的局限性。因为线程任务对象只是实现了接口,还可以继续继承其他类和实现其他接口。
3. 实现解耦操作,线程任务对象代码可以被多个线程共享,代码和线程独立。
4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。适合做线程池。
*/
// 定义一个线程任务类实现Runnable接口。
class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName() + "=>" + i);
}
}
}
public class ThreadDemo01 {
public static void main(String[] args) {
// 创建一个线程任务对象
Runnable target = new MyRunnable();
// 将线程任务对象包装成线程对象
Thread t1 = new Thread(target);
// 启动线程
t1.start();
// 创建线程对象
Thread t2 = new Thread(target);
// 启动线程
t2.start();
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName() + "=>"+i);
}
}
}
2.2.3 代码示例(匿名内部类)
package cn.guardwhy_03;
/**
匿名内部类简化写法!
*/
public class ThreadDemo02 {
public static void main(String[] args) {
// 直接创建Runnable的线程任务对象的匿名内部类形式
/*
Runnable target = new Runnable() {
@Override
public void run() {
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName() + "=>" + i);
}
}
};
Thread t1 = new Thread(target);
t1.start();
*/
// 匿名形式..
new Thread(new Runnable() {
@Override
public void run() {
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName() + "=>"+i);
}
}
}).start();
// 遍历操作
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName()+"=>"+i);
}
}
}
注意
启动线程不能直接使用run()方法?
因为run()方法仅仅是封装线程执行代码,直接调用是普通方法。Start( )方法首先启动了线程,然后由JVM去调用该线程的run()方法。
JVM虚拟机的启动是多线程的,原因是垃圾回收线程要先启动,否则容易出现内存溢出。
2.2.4 Thread和Runnable区别
1) Thread类是Runnable接口的子类,但是Thread类中并且没有完全实现Runnable接口中的run( )方法。
2)如果一个类继承Thread类,不适合于多个线程共享资源,实现Runnable接口,就可以方便地实现资源共享。
2.3 Callable接口
2.3.1 基本概念
2.3.2 原理分析
2.3.3 代码示例(传统方式)
定义一个线程任务类实现Callable接口。
package cn.guardwhy_04;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
创建线程的方式三:
a.定义一个线程任务类实现Callable接口。
b.重写call()方法。
c.把线程任务对象包装成一个未来任务对象。
d.把未来任务对象包装成一个线程对象。
e.调用线程对象的start()方法启动线程。
优缺点:
缺点:编码复杂。
优点:全是优点。
可以继续继承其他类。可以做线程池。可以得到线程返回的结果。可以做资源共享操作。
*/
//a.定义一个线程任务类实现Callable接口,申明返回值类型
class MyCallable implements Callable<String>{
@Override
public String call() throws Exception {
// 定义计数器
int count = 0;
for(int i=0; i<10; i++){
// 1-10的和
count += (i+1);
System.out.println(Thread.currentThread().getName()+"=>"+i);
}
return Thread.currentThread().getName()+"求和结果:" + count;
}
}
public class ThreadDemo01 {
public static void main(String[] args) {
// 把线程任务对象包装成一个未来任务对象。
MyCallable call = new MyCallable();
/**
* 未来任务对象: FutureTask
* 1.可以通过未来任务对象去获取线程执行的结果。
* 2.未来任务对象其实就是一个Runnable的对象。
*/
FutureTask<String> target = new FutureTask<>(call);
// 将未来任务对象包装成一个线程对象
Thread t1 = new Thread(target);
// 调用线程对象的start()方法启动线程
t1.start();
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName() + "=>" + i);
}
try {
// 线程的执行的结果
String result = target.get();
System.out.println("线程执行结果:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.3.4 代码示例(jdk1.8)
Callable底层源码
package cn.guardwhy.List01;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class CallableTest01 {
public static void main(String[] args) throws Exception {
/*
* 步骤:
* 1. new Thread(new Runable()).start;
* 2. new Thread(new FutureTask<V>()).start;
* 3. new Thread(new FutureTask<V>(Callable)).start;
*/
// 创建线程对象
MyThread thread = new MyThread();
// 适配类:未来任务对象 FutureTask
FutureTask futureTask = new FutureTask(thread);
// 线程操作
new Thread(futureTask, "kobe").start();
// 第二次调用执行,会有结果缓存,不用再次计算
new Thread(futureTask, "curry").start();
// 获取操作,get方法可能会产生阻塞,放到最后
Integer result = (Integer) futureTask.get();
System.out.println(result);
}
}
class MyThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("call方法被调用");
// 耗时操作
return 666;
}
}
2.3.5 Callable与Runnable区别
- 是否有返回值, 是否抛异常。
- 方法不一样,一个是call,一个是run。
3- 线程状态
3.1 线程的生命周期
3.1.1 图示生命周期
3.1.2 线程的状态
1)新建状态 :使用new关键字创建之后进入的状态,此时线程并没有开始执行。
2)就绪状态:调用start方法后进入的状态,此时线程还是没有开始执行。
3) 运行状态:使用线程调度器调用该线程后进入的状态,此时线程开始执行,当线程的时间片执行完毕后任务没有完成时回到就绪状态。
4) 阻塞状态:当线程执行的过程中发生了阻塞事件进入的状态,如:sleep方法。阻塞状态解除后进入就绪状态。
5)死亡状态:线程调用stop( )方法时或者run( )方法执行结束后,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。
3.1.3 生命周期路线
新建-就绪-运行-死亡
新建-就绪-运行-就绪-运行-死亡
新建-就绪-运行-就绪-运行-死亡
新建-就绪-运行-其他阻塞-就绪-运行-死亡
新建-就绪-运行-同步阻塞-就绪-运行-死亡
新建-就绪-运行-等待阻塞-同步阻塞-就绪-运行-死亡
3.2 线程操作方法
2.2.1 常用方法API
方法声明 | 功能介绍 |
---|---|
Thread( ) | 使用无参的方式构造对象 |
Thread(String name) | 根据参数指定的名称来构造对象 |
Thread(Runnable target,String name) | 根据参数指定引用和名称来构造对象。 |
void start( ) | 用于启动线程,Java虚拟机会自动调用该线程的run方法 |
long getId( ) | 获取调用对象所表示线程的编号。 |
Thread currentThread( ) | 获取当前正在执行线程的引用。 |
int getPriority( ) | 发挥线程的优先级 |
boolean isInterrupted( ) | 判断目前线程是否被中断 |
void join( ) | 等待线程死亡 |
String getName( ) | 返回线程的名称 |
void yield( ) | 将目前正在执行的线程暂停 |
3.2.2 取得线程名称
package cn.guardwhy_02;
/**
Thread多线程常用API:
线程是有默认名字的:子线程的名称规则Thread_索引, main线程的默认名称就是main
1.public void setName(String name):给线程对象取名字。
2.public String getName():返回线程对象的名字。
3.public static Thread currentThread(): 获取当前线程对象,这个代码是哪个线程在执行就返回哪个线程对象。
*/
// 定义一个线程类继承Thread类,线程类不是线程对象,是用来创建线程对象的。
class MyThread extends Thread{
@Override
public void run() {
for(int i=0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + "=>" + i);
}
}
}
public class ThreadDemo01 {
public static void main(String[] args) {
// 创建一个子线程对象
MyThread t1 = new MyThread();
// 设置线程
t1.setName("1号线程");
// 启动线程
t1.start();
// 输出t1线程对象
// System.out.println(t1.getName());
// 创建t2子线程对象
MyThread t2 = new MyThread();
t2.setName("2号线程..");
// 启动线程
t2.start();
// 输出t2线程对象
// System.out.println(t2.getName());
// 返回当前线程对象,这个代码是哪个线程在执行就返回哪个线程对象。
Thread main = Thread.currentThread();
// System.out.println(main.getName());
main.setName("最牛逼的线程");
for (int i=0; i< 10; i++){
System.out.println(main.getName()+ "=>" + i);
}
}
}
3.2.3 有参构造器
通过有参数构造器为线程对象取名字
package cn.guardwhy_02;
/**
目标:通过有参数构造器为线程对象取名字(拓展)
Thread父类的有参数构造器: public Thread(String name):
*/
// 定义一个线程类继承Thread类
class MyThread02 extends Thread{
// 代参构造器
public MyThread02(String name) {
super(name);
}
// 重写Thread类的run()方法
@Override
public void run() {
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName() + "=>" + i);
}
}
}
public class ThreadDemo02 {
public static void main(String[] args) {
// 创建一个子进程对象
MyThread02 t1 = new MyThread02("1号线程");
// 启动线程
t1.start();
MyThread02 t2 = new MyThread02("2号线程");
// 启动线程
t2.start();
// 获取当前线程对象
Thread main = Thread.currentThread();
for(int i=0; i<10; i++){
System.out.println(main.getName() + "=>"+i);
}
}
}
3.2.4 强制运行
1) 代码示例
package cn.thread.demo01;
class MyThread implements Runnable{
@Override
public void run() {
// 覆写run()方法
for(int i=0; i<10; i++){
// 输出线程名称
System.out.println(Thread.currentThread().getName() + "运行 -->" + i);
}
}
}
public class ThreadJoinDemo01 {
public static void main(String[] args) {
// 实例化对象
MyThread mt = new MyThread();
// 实例化Thread对象
Thread t = new Thread(mt, "线程");
// 线程启动
t.start();
// 循环10次
for(int i=0; i<10; i++){
// 判断变量内容
if(i > 3){
try {
// 线程t进行强制运行
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Main 线程运行 -->" + i);
}
}
}
2) 执行结果
3.2.5 线程休眠
package cn.guardwhy_10;
/**
线程休眠
Thread.sleep(5000):参数是毫秒,让当前所在线程对象休眠5s。
*/
public class ThreadDemo01 {
public static void main(String[] args) {
// 条件遍历
for(int i=0; i<10; i++){
System.out.println("输出:" + i);
if(i == 5){
try {
// 让当前线程休眠5s,休眠是不释放锁的。
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
4- 线程同步机制
4.1 基本概念
- 当多个线程同时访问同一种共享资源时,可能会造成数据的覆盖等不一致性问题,此时就需要对线程之间进行通信和协调。
- 多个线程并发读写同一个临界资源时会发生线程并发安全问题。
- 异步操作:多线程并发的操作,各自独立运行。同步操作:多线程串行的操作,先后执行的顺序。
4.2 线程安全问题
线程问题的核心原因:多个线程操作同一个共享资源的时候可能出现线程安全问题。
4.2.1代码示例
1)账户对象
package cn.guardwhy_05;
public class Account {
// 卡号
private String cardId;
// 余额
private double money;
// 无参构造器
public Account() {
}
// 代参构造器
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
/*
get/set方法
*/
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 void drawMoney(double money){
// 1.先拿到是谁来取钱:拿到当前线程的名字即可,名字是谁就是谁来取钱
String name = Thread.currentThread().getName();
// 2.判断余额是否足够
if(this.money >= money){
// 钱够了
System.out.println(name + "来取钱,余额足够,吐出:" + money);
// 更新余额
this.money -= money;
System.out.println(name + "取钱剩余:" + this.money);
}else {
// 钱不够
System.out.println(name + "来取钱,余额不足..");
}
}
}
2) 取钱的线程类
package cn.guardwhy_05;
/**
* 取钱的线程类
*/
public class DrawThread extends Thread{
// 定义一个成员变量接收账户对象
private Account acc;
// 带参构造器
public DrawThread(String name, Account acc) {
super(name);
this.acc = acc;
}
@Override
public void run() {
// 去账户acc中取钱
acc.drawMoney(10000);
}
}
3)转账功能
package cn.guardwhy_05;
/**
先模拟一个线程安全问题的案例:转账功能。
分析:整存整取。
(1)定义一个账户(余额,卡号)。
(2)定义一个取钱的线程类
(3)创建一个账户对象,创建2个线程对象,去这个账户对象取钱10000
总结:
多个线程操作同一个共享资源的时候可能出现线程安全问题。
*/
public class ThreadSaveDemo01 {
public static void main(String[] args) {
// 1.创建一个共享资源:是一个账户对象,这个对象必须只有一个。
Account acc = new Account("ICBC-162", 10000);
// 创建2个线程对象代表curry和james
DrawThread curry = new DrawThread("curry", acc);
// 启动线程
curry.start();
DrawThread james = new DrawThread("james", acc);
// 启动线程
james.start();
}
}
4) 执行结果
4.2.2 解决方案
1)引发原因
- 当两个线程同时对同一个账户进行取款时,导致最终的账户余额不合理。
- 线程一执行取款时还没来得及将取款后的余额写入后台,线程二就已经开始取款。
2)解决方案
- 让线程一执行完毕取款操作后,再让线程二执行即可,将线程的并发操作改为串行操作。
4.3 线程同步
4.3.1 线程同步的作用
-
用于解决线程安全问题,线程同步就是指线程安全了。线程同步解决线程安全问题的核心思想。
-
线程同步就是让多个线程实现先后有序的访问共享资源,每次只能一个线程执行完毕,另一个线程才能进行。
4.3.2 同步代码块
1) 账户对象
package cn.guardwhy_06;
public class Account {
// 卡号
private String cardId;
// 余额
private double money;
// 无参构造器
public Account() {
}
// 代参构造器
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
/*
get/set方法
*/
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 void drawMoney(double money){
// 1.先拿到是谁来取钱:拿到当前线程的名字即可,名字是谁就是谁来取钱
String name = Thread.currentThread().getName();
// 2.判断余额是否足够
synchronized (this){
if(this.money >= money){
// 钱够了
System.out.println(name + "来取钱,余额足够,吐出:" + money);
// 更新余额
this.money -= money;
System.out.println(name + "取钱剩余:" + this.money);
}else {
// 钱不够
System.out.println(name + "来取钱,余额不足..");
}
}
}
}
2) 取钱的线程类
package cn.guardwhy_06;
/**
* 取钱的线程类
*/
public class DrawThread extends Thread{
// 定义一个成员变量接收账户对象
private Account acc;
// 带参构造器
public DrawThread(String name, Account acc) {
super(name);
this.acc = acc;
}
@Override
public void run() {
// 去账户acc中取钱
acc.drawMoney(10000);
}
}
3)转账功能
package cn.guardwhy_06;
/**
同步代码块:
思想:把“出现线程安全问题的核心代码”给锁起来,每次只能进入一个线程,
其他线程必须在外面等这个线程执行完毕以后,才能进入执行,这样就线程安全了。
格式:
synchronized(锁对象){
出现线程安全问题的核心代码。
}
锁对象:原则上可以是任意唯一的Java对象。
理论上:在实例方法中推荐用this作为锁.在静态方法中推荐用类名.class字节码文件作为锁对象
*/
public class ThreadSaveDemo01 {
public static void main(String[] args) {
// 1.创建一个共享资源:是一个账户对象,这个对象必须只有一个。
Account acc = new Account("ICBC-162", 10000);
// 创建2个线程对象代表curry和james
DrawThread curry = new DrawThread("curry", acc);
// 启动线程
curry.start();
DrawThread james = new DrawThread("james", acc);
// 启动线程
james.start();
}
}
4) 执行结果
4.3.3 同步方法
1) 账户对象
package cn.guardwhy_07;
public class Account {
// 卡号
private String cardId;
// 余额
private double money;
// 无参构造器
public Account() {
}
// 代参构造器
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
/*
get/set方法
*/
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 synchronized void drawMoney(double money){
// 1.先拿到是谁来取钱:拿到当前线程的名字即可,名字是谁就是谁来取钱
String name = Thread.currentThread().getName();
// 2.判断余额是否足够
if(this.money >= money){
// 钱够了
System.out.println(name + "来取钱,余额足够,吐出:" + money);
// 更新余额
this.money -= money;
System.out.println(name + "取钱剩余:" + this.money);
}else {
// 钱不够
System.out.println(name + "来取钱,余额不足..");
}
}
}
2) 取钱的线程类
package cn.guardwhy_07;
public class DrawThread extends Thread{
// 定义一个成员变量接收账户对象
private Account acc;
// 带参构造器
public DrawThread(String name, Account acc) {
super(name);
this.acc = acc;
}
@Override
public void run() {
// 去账户acc中取钱
acc.drawMoney(10000);
}
}
3) 转账功能
package cn.guardwhy_07;
/**
同步方法:
思想:把"出现线程安全问题的核心方法"给锁起来,每次只能进入一个线程,
其他线程必须在外面等这个线程执行完毕以后,才能进入执行,这样就线程安全了。
只需要在方法上加上一个 synchronized 关键字即可!
原理:同步方法的原理与同步代码块的原理是一样的,只是同步方法是把整个方法体代码都锁起来,同步方法默认也是有锁对象的。
如果同步的方法是实例方法,默认用this作为锁对象。如果同步的方法是静态方法,默认用类名.class作为锁对象。
*/
public class ThreadSaveDemo01 {
public static void main(String[] args) {
// 1.创建一个共享资源:是一个账户对象,这个对象必须只有一个。
Account acc = new Account("ICBC-162", 10000);
// 创建2个线程对象代表curry和james
DrawThread curry = new DrawThread("curry", acc);
// 启动线程
curry.start();
DrawThread james = new DrawThread("james", acc);
// 启动线程
james.start();
}
}
4) 执行结果
5- 死锁
5.1 产生死锁
5.1.1 基本概念
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
基本结构
线程一执行的代码:
public void run(){
synchronized(a){
//持有对象锁a,等待对象锁b
synchronized(b){
编写锁定的代码;
}
}
}
线程二执行的代码:
public void run(){
synchronized(b){
//持有对象锁b,等待对象锁a
synchronized(a){
编写锁定的代码;
}
}
}
注意:在以后的开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用!
5.1.2 死锁产生条件
互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
循环等待,即存在一个等待队列:p1要p2的资源,p2要p1的资源。这样就形成了一个等待环路。
5.1.3 代码示例
package cn.guardwhy_11;
/**
实现死锁一般需要进行锁资源的嵌套才会出现死锁。
*/
public class ThreadDead {
// 定义两个静态资源对象
public static Object resources1 = new Object();
public static Object resources2 = new Object();
public static void main(String[] args) {
// 实现死锁现象至少存在两个线程
new Thread(new Runnable() {
@Override
public void run() {
synchronized (resources1){
System.out.println("线程对象1对资源1上锁,占用资源1");
System.out.println("线程对象1开始请求资源2");
try {
// 线程休眠
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resources2){
System.out.println("线程对象1对资源2上锁,占用资源2");
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (resources2){
System.out.println("线程对象2对资源2上锁,占用资源2");
System.out.println("线程对象2开始请求资源1");
try {
// 线程休眠
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resources1){
System.out.println("线程对象2对资源1上锁,占用资源1");
}
}
}
}).start();
}
}
5.1.4 执行结果
5.2 线程通信
5.2.1 Object类常用方法
方法声明 | 功能介绍 |
---|---|
void wait( ) | 用于使得线程进入等待状态,直到其它线程调用notify()或notifyAll()方法,此方法必须锁对象调用。 |
void wait(long timeout) | 用于进入等待状态,直到其它线程调用方法或参数指定的毫秒数已经过去为止。 |
void notify() | 唤醒当前锁对象上等待状态的某个线程 此方法必须锁对象调用。 |
void notifyAll( ) | 唤醒当前锁对象上等待状态的全部线程 此方法必须锁对象调用。 |
5.2.2 生产者及消费者
1)账户对象
package cn.guardwhy_09;
/**
* 账户对象
*/
public class Account {
// 卡号
private String cardId;
// 余额
private int money;
// 无参构造器
public Account() {
}
// 代参构造器
public Account(String cardId, int money) {
this.cardId = cardId;
this.money = money;
}
/**
* get/set方法
* @return
*/
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
// 取钱方法
public synchronized void drawMoney(int money){
try {
// 1.先拿到是谁来取钱:拿到当前线程的名字即可,名字是谁就是谁来取钱
String name = Thread.currentThread().getName();
// 2.判断余额是否足够
if(this.money >= money){
// 更新余额
this.money -= money;
System.out.println(name + "来取钱,余额足够.吐出:" + money + "元,剩余" + this.money + "元");
this.notify(); // 钱已经取完了,暂停自己唤醒其他线程!
// 等待自己
this.wait();
}else {
this.notify(); // 没钱了.唤醒其他线程!
// 等待自己
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 存钱方法
public synchronized void saveMoney(int money){
try {
String name = Thread.currentThread().getName();
// 判断是否有钱
if(this.money == 0){
// 没钱,需要存钱
this.money += money;
System.out.println(name + "来存钱" + money + "元成功, 剩余" + this.money + "元");
this.notify(); // 钱已经取完了,暂停自己,唤醒其他线程
this.wait(); // 等待自己
}else {
this.notify(); // 唤醒其他线程
this.wait(); // 等待自己
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
2) 存钱的线程类
package cn.guardwhy_09;
/**
存钱的线程类。
*/
public class SaveThread extends Thread{
// 定义一个成员变量接收账户对象
private Account acc;
// 带参构造器
public SaveThread(String name, Account acc) {
super(name);
this.acc = acc;
}
@Override
public void run() {
// 3个爸爸来反复的存钱.
while (true){
try {
// 线程休眠
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
acc.saveMoney(10000);
}
}
}
3) 取钱的线程类
package cn.guardwhy_09;
/**
取钱的线程类。
*/
public class DrawThread extends Thread{
// 定义一个成员变量接收账户对象
private Account acc;
// 带参构造器
public DrawThread(String name, Account acc) {
super(name);
this.acc = acc;
}
@Override
public void run() {
// curry和james来取钱
while (true){
// 线程休眠
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 去账户acc中取钱
acc.drawMoney(10000);
}
}
}
4) 线程通信
package cn.guardwhy_09;
/**
线程通信:多个线程因为在同一个进程中,所以互相通信比较容易。
线程通信的经典模型:生产者与消费者问题。
生产者负责生成商品,消费者负责消费商品。
生成不能不剩,消费不能没有,是一个同步模型。
线程通信必须先保证线程安全,否则代码会报出异常!!
模拟一个案例:
curry和james有一个共同账户:共享资源
他们有3个爸爸(亲爸,岳父,干爹)给他们存钱。
模型:curry和james去取钱,如果有钱就取出,等待自己,唤醒他们3个爸爸们来存钱
他们的爸爸们来存钱,如果发现有钱就不存,没钱就存钱,然后等待自己,唤醒孩子们来取钱。
整存整取 10000元。
分析:
生产者:亲爸,岳父,干爹
消费者:curry,james
共享资源:账户对象。
*/
public class ThreadCommunication01 {
public static void main(String[] args) {
// 创建一个共享资源账户对象
Account acc = new Account("ISBC-162530", 0);
// 定义两个取钱线程代表curry和james
new DrawThread("curry", acc).start();
new DrawThread("james", acc).start();
// 定义三个存钱线程,分别代表亲爸,岳父,干爹
new SaveThread("亲爸", acc).start();
new SaveThread("干爹", acc).start();
new SaveThread("岳父", acc).start();
}
}
5)执行结果
5.3 线程同步(Lock锁)
5.3.1 常用的方法
方法声明 | 功能介绍 |
---|---|
ReentrantLock( ) | 使用无参方式构造对象 |
void lock( ) | 获取锁 |
void unlock( ) | 释放锁 |
与synchronized方式的比较
- Lock是显式锁,需要手动实现开启和关闭操作,而synchronized是隐式锁,执行锁定代码后自动释放。
- Lock只有同步代码块方式的锁,而synchronized有同步代码块方式和同步方法两种锁。
- 使用Lock锁方式时,Java虚拟机将花费较少的时间来调度线程,因此性能更好。
5.3.2 代码示例
1) 账户对象
package cn.guardwhy_08;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
// 卡号
private String cardId;
// 余额
private double money;
// 无参构造器
public Account() {
}
// 代参构造器
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
// 创建一把锁对象:必须保证这个对象唯一
private final Lock lock = new ReentrantLock();
/***
* get/set方法
* @return
*/
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 void drawMoney(double money){
// 1.先拿到是谁来取钱:拿到当前线程的名字即可,名字是谁就是谁来取钱
String name = Thread.currentThread().getName();
// 2.判断余额是否足够,加锁操作
lock.lock();
try {
if(this.money >= money){
// 钱够了
System.out.println(name + "来取钱,余额足够,吐出:" + money);
// 更新余额
this.money -= money;
System.out.println(name + "取钱剩余:" + this.money);
}else {
// 钱不够
System.out.println(name + "来取钱,余额不足..");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); // 解锁操作
}
}
}
2) 取钱的线程类
package cn.guardwhy_08;
/**
* 取钱的线程类
*/
public class DrawThread extends Thread{
// 定义一个成员变量接收账户对象
private Account acc;
// 带参构造器
public DrawThread(String name, Account acc) {
super(name);
this.acc = acc;
}
@Override
public void run() {
// 去账户acc中取钱
acc.drawMoney(10000);
}
}
3) 转账功能
package cn.guardwhy_08;
/**
Lock锁
java.util.concurrent.locks.Lock
Lock锁也称同步锁,加锁与释放锁方法如下:
- public void lock() :加同步锁。
- public void unlock():释放同步锁。
*/
public class ThreadSaveDemo01 {
public static void main(String[] args) {
// 1.创建一个共享资源:是一个账户对象,这个对象必须只有一个。
Account acc = new Account("ICBC-162", 10000);
// 创建2个线程对象代表curry和james
DrawThread curry = new DrawThread("curry", acc);
// 启动线程
curry.start();
DrawThread james = new DrawThread("james", acc);
// 启动线程
james.start();
}
}
4) 执行结果
6- 线程池
6.1 线程池概述
6.1.1 什么是线程池
线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用。省去了频繁创建和销毁线程对象的操作,无需反复创建线程而消耗过多资源。
6.1.2 线程池好处
1.降低资源消耗,减少了创建和销毁线程的次数。每个工作线程都可以被重复利用,可执行多个任务。
2.提高响应速度,线程池的核心思想:线程复用,同一个线程可以被重复使用。
3.提高线程的可管理性(线程池可以约束系统最多只能有多少个线程,不会因为线程过多而死机)
6.2 创建线程池
6.2.1 创建方式一
package cn.guardwhy_12;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
线程池在Java中的代表: ExecutorService
创建线程池的API:
java.util.concurrent.Executors类下:
-- public static ExecutorService newFixedThreadPool(int nThreads):
返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
总结:
线程池启动后是不会死亡的,因为后续还要重复使用的。
void shutdown():会等全部线程执行完毕才关闭。比较友好!
List<Runnable> shutdownNow():立即关闭,不管是否执行完毕
*/
class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=0; i<3; i++){
System.out.println("线程" + Thread.currentThread().getName()+" => " + i);
}
}
}
public class ThreadPoolsDemo01 {
public static void main(String[] args) {
// 创建一个线程池:线程池固定放置三个线程
ExecutorService pools = Executors.newFixedThreadPool(3);
// 给线程池提交任务,提交任务的时候会自动创建线程对象
Runnable target = new MyRunnable();
// 提交任务会自动创建线程对象,并自动启动
pools.submit(target);
pools.submit(target);
pools.submit(target);
// 这里不会再创建线程了,因为线程池已经满了,这里会复用之前的线程。
pools.submit(target);
// 全部线程执行完毕才关闭
pools.shutdown();
}
}
6.2.2 创建方式二
package cn.guardwhy_12;
import java.util.concurrent.*;
/**
线程池的创建方式二。
线程池在Java中的代表: ExecutorService
创建线程池的API:
java.util.concurrent.Executors类下:
-- public static ExecutorService newFixedThreadPool(int nThreads):
返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
往线程池中创建线程的API:
1.public Future<?> submit(Runnable task)
2.<T> Future<T> submit(Callable<T> task)
总结: Callable接口创建线程对象是可以返回线程执行的结果的。
*/
class Mycallable implements Callable<String>{
// 定义变量
private int n;
// 带参构造器
public Mycallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
// 定义计数器
int count = 0;
for(int i=1; i<=n; i++){
count += i;
}
return Thread.currentThread().getName()+ "=> 1-" + n + "和是: " + count;
}
}
public class ThreadPoolsDemo02 {
public static void main(String[] args) {
// 1.创建一个线程池:线程池固定放置三个线程
ExecutorService pools = Executors.newFixedThreadPool(3);
// 2.提交任务给线程池
Mycallable t1 = new Mycallable(10);
Mycallable t2 = new Mycallable(20);
Mycallable t3 = new Mycallable(30);
Mycallable t4 = new Mycallable(40);
Future<String> rs1 = pools.submit(t1);
Future<String> rs2 = pools.submit(t2);
Future<String> rs3 = pools.submit(t3);
Future<String> rs4 = pools.submit(t4);
try {
System.out.println(rs1.get());
System.out.println(rs2.get());
System.out.println(rs3.get());
System.out.println(rs4.get());
} catch (Exception e) {
e.printStackTrace();
}
pools.shutdown(); // 关闭线程池
}
}