第4章Java多线程+网银取款案例精讲
4-2进程概述及特性
1、进程:
一个正在运行中的程序就是一个进程;
进程的特性:独立性、动态性、并发性:
- 1.独立性
每个进程都拥有自己独立的内存空间和其他相关资源,一般来说进程之间是很难通信的,即使可以通信也是比较麻烦
需要通过一些进程通信的手段才可以实现进程通信。 - 2.动态性
程序是静止的,运行中的程序才是动态的,进程就是运行中的程序 - 3.并发性
CPU是分时执行的。CPU时间片与内存空间按一定的时间间隔,轮流地切换给各进程使用,
因为CPU执行的速度太快了,用户根本看不出来,我们会看到一个假象,我们会认为同一时刻
有多个进行在同时执行,前期是单核CPU;
并发:同一时刻CPU实际上只在处理一个进程,当CPU处理时间片到了,CPU就会被另一个进程占用
因为CPU这种轮流执行进程的速度很快,所以我们相当于看到程序在同时运行。 这就是并发。
如果想要同一时刻运行多个进程(称之为并行),操作系统就必须有对应的多个CPU。
4-3线程概述以及进程的关系
2、线程:
一个进程中可以包含多个线程,线程不能独立存在,必须依赖与进程,线程之间通信比较简单。因为他们可以共享同一块内存区域
特点: 基本不占用内存空间和资源,动态性,并发性。
★多线程的好处:
a.多线程相对于进程占用内存和资源非常少,节约内存。
b.也可以并发执行。
c.线程之间很容易进行通信,所以效率高。
d.java对多线程的支持十分的完美。
4-4线程的创建以及执行
创建线程的三种方式
-1、第一种方式继承Thread类,重写Thread类中的run方法,还需要调用start方法,start方法相当于通知CPU,线程已经就绪,CPU在合适的时间点调用该线程的run方法;我们程序中的main方法,我们称之为主线程
-2、创建线程的第二种方式,实现Runnable接口,并重写run方法,
创建实例之后,将该实例包装成Thread实例,继续调用start方法让线程就绪,因为Runnable是一个函数式接口,因此可以通过Lambda表达式,进行Runnable实例的创建;
-3、创建线程的第三种方式,实现Callable,重写该接口的call方法,call方法不同于run方法,run方法没有返回值,而call方法有返回值;
第一步,创建Callable实例,重写call方法
第二步,将Callable实例传入FutureTask构造器中,得到FutureTask实例
第三步,创建Thread实例,将FutureTask实例传入Thread构造器中,再让线程就绪
总结:通过继承Thread创建线程与实现Runnable接口与实现Callable接口创建线程的区别;
1、相对而言 继承Thread 创建线程代码是最简单的;
2、如果我们的类继承了Thread那就不能再去继承其他类,因为java是单继承,
如果我们通过实现接口Runnable或者Callable来创建线程,我们的类还有权利去继承其他类;
3、实现Runnable或者Callable接口,可以让多个线程共享同一份资源
4、当我们需要线程执行完毕之后有返回值|信息返回,那么需要 实现 Callable接口
线程创建的第一种方式
/**
*创建线程的方式有三种
*1、继承 Thread,并重写Thread类中的run方法,通过调用start方法,让线程就绪
*
*/
public class MyThread_01 extends Thread{
/*
* 重写线程父类Thread的run方法
*/
@lombok.SneakyThrows
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 50; i++) {
//System.out.println("线程名字为:"+Thread.currentThread().getName()+"=====i===="+i);
System.out.println("线程名字为:"+this.getName()+"=====i===="+i);
Thread.sleep(20);
}
}
/**
* main方法是程序的入口方法
* main方法被称为主线程
*/
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
//手动创建线程实例
MyThread_01 thread01 = new MyThread_01();
//设置线程的名字
thread01.setName("线程一");
//start方法让线程就绪 相当于告诉CPU,线程已经就绪,这个时候CPU会在合适的时间点调用该线程的run方法
thread01.start();
for (int j = 0; j < 50; j++) {
System.out.println("线程名字为:"+Thread.currentThread().getName()+"=====j===="+j);
Thread.sleep(20);
}
System.out.println("------------程序执行完毕---------");
}
}
线程创建的第二种方式
创建线程的第二种方式,实现Runnable接口,并重写run方法,
创建实例之后,将该实例包装成Thread实例,继续调用start方法让线程就绪,因为Runnable是一个函数式接口,因此可以通过Lambda表达式,进行Runnable实例的创建;
Runnable接口中没有start方法,所以需要装成Thread实例,来调用Thread start 方法来使线程进入就绪状态
/**
*创建线程的方式有三种
*2、实现Runnable接口,并重写run方法
*
*/
public class MyThread_02 implements Runnable{
/*
* 重写Runnable接口中的run方法
*/
@SneakyThrows
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 50; i++) {
//Thread.currentThread():获取当前线程 getName:获取当前线程的名字
System.out.println("线程名字为:"+Thread.currentThread().getName()+"=====i===="+i);
Thread.sleep(10);
}
}
public static void main(String[] args) throws InterruptedException {
//=============1====================//
//1.创建MyThread_02实例
MyThread_02 mythread = new MyThread_02();
//创建线程实例,对Runnable进行包装
Thread thread = new Thread(mythread);
//设置线程的名字
thread.setName("打开小明聊天窗口");
//让线程就绪
thread.start();
//==========2=========================//
//==================1=======================/
//2. 使用匿名内部类的方式实例一个Runnable接口类
Runnable runnable = new Runnable() {
/*
* 重写Runnable的run方法
*/
@SneakyThrows
@Override
public void run() {
// TODO Auto-generated method stub
for (int k = 0; k < 50; k++) {
//Thread.currentThread():获取当前线程 getName:获取当前线程的名字
System.out.println("线程名字为:"+Thread.currentThread().getName()+"=====k===="+k);
Thread.sleep(10);
}
}
};
//将Runnable实例包装成 Thread的实例
Thread thread02 = new Thread(runnable);
thread02.setName("打开阿毛聊天窗口");
//让线程就绪
thread02.start();
//======================2========================//
//=================1=========================//
//3. 因为Runnable是一个函数式接口,用Lambda表达式进行简化
Runnable runnable02 = () ->{
for (int k = 0; k < 50; k++) {
//Thread.currentThread():获取当前线程 getName:获取当前线程的名字
System.out.println("线程名字为:"+Thread.currentThread().getName()+"=====k===="+k);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//将Runnable实例包装成 Thread的实例
Thread thread03 = new Thread(runnable02);
thread03.setName("打开无忌聊天窗口");
//让线程就绪
thread03.start();
//=====================2================//
for (int i = 0; i < 50; i++) {
//Thread.currentThread():获取当前线程 getName:获取当前线程的名字
System.out.println("线程名字为:"+Thread.currentThread().getName()+"=====i===="+i);
Thread.sleep(10);
}
}
}
线程创建的第三种方式
创建线程的第三种方式,实现Callable,重写该接口的call方法,call方法不同于run方法,run方法没有返回值,而call方法有返回值;
第一步,创建Callable实例,重写call方法
第二步,将Callable实例传入FutureTask构造器中,得到FutureTask实例
第三步,创建Thread实例,将FutureTask实例传入Thread构造器中,再让线程就绪
/**
*创建线程的方式有三种
*1、实现Callable接口,并重写call方法
*
*
*/
public class MyThread_03 implements Callable<Integer>{
/*
* 重写Callable接口中的call方法
*/
@Override
public Integer call() throws Exception {
// TODO Auto-generated method stub
int count = 0;
for (int k = 0; k < 50; k++) {
//Thread.currentThread():获取当前线程 getName:获取当前线程的名字
System.out.println("线程名字为:"+Thread.currentThread().getName()+"=====k===="+k);
count++;
Thread.sleep(20);
}
return count;
}
public static void main(String[] args) throws InterruptedException {
//1、创建 MyThread_03实例
MyThread_03 callable = new MyThread_03();
//2、创建FutureTask实例 FutureTask就是Runnable实现类
FutureTask<Integer> future = new FutureTask<>(callable);
//3.创建Thread实例,对Runnable进行包装
Thread thread = new Thread(future);
thread.setName("线程一");
thread.start();
for (int i = 0; i < 50; i++) {
//Thread.currentThread():获取当前线程 getName:获取当前线程的名字
System.out.println("线程名字为:"+Thread.currentThread().getName()+"=====i===="+i);
Thread.sleep(50);
}
try {
//用FutureTask对象在,当线程执行完毕之后获取线程执行完毕后的返回值
int count = future.get();
System.out.println("count:"+count);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
4-7线程的生命周期
4-8线程的常用方法
1、currentThread : 静态方法,获取当前线程
2、getName:获取线程的名字,实例方法
3、getId:获取线程id,线程id是唯一
4、setPriority : 设置线程优先级,如果不设置,默认为5 ,
总共有 三个 1 5 10 值越大说明,被CPU调用的机会越大;
6、setName:设置线程名字,注意没有setId方法,因为线程的id是自动分配的,并且是唯一,不需要人为设置
7、getPriority :获取线程优先级
8、start方法让线程就绪
9、stop方法用于结束当前线程
10、run:线程的主方法,线程在执行的时候,就是在执行run方法
11、sleep方法:让线程处于睡眠状态
/**
* 线程中常用方法讲解
*/
public class ThreadTest extends Thread{
/* (non-Javadoc)
* @see java.lang.Thread#run()
*/
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 50; i++) {
//获取当前线程实例
Thread thread = Thread.currentThread();
//获取线程的名字,可以重复
String name = thread.getName();
//获取线程id,线程id是不会重复的,好比人的省份证一样不会重写
long id = thread.getId();
//获取线程的优先级 如果线程的优先级越大,说明获取CPU的执行机会越大
int priority = thread.getPriority();
System.out.println("name:"+name+" id:"+id +" 优先级:"+priority);
}
}
/**
* @param args
*/
public static void main(String[] args) {
ThreadTest thread = new ThreadTest();
thread.setName("线程一");
//设置线程的优先级
thread.setPriority(Thread.MIN_PRIORITY);
//设置为后台线程 最后死亡,最后死亡不代表最后执行完毕
thread.setDaemon(true);
//让线程就绪
thread.start();
for (int i = 0; i < 50; i++) {
//获取当前线程实例
Thread thread2 = Thread.currentThread();
//获取线程的名字,可以重复
String name = thread2.getName();
//获取线程id,线程id是不会重复的,好比人的省份证一样不会重写
long id = thread2.getId();
//获取线程的优先级 如果线程的优先级越大,说明获取CPU的执行机会越大
int priority = thread2.getPriority();
System.out.println("name:"+name+" id:"+id +" 优先级:"+priority);
}
}
}
总结:
- 通过继承Thread创建线程与实现Runnable接口与实现Callable接口创建线程的区别;
1、相对而言 继承Thread 创建线程代码是最简单的;
2、如果我们的类继承了Thread那就不能再去继承其他类,因为java是单继承,
如果我们通过实现接口Runnable或者Callable来创建线程,我们的类还有权利去继承其他类;
3、实现Runnable或者Callable接口,可以让多个线程共享同一份资源
4、当我们需要线程执行完毕之后有返回值|信息返回,那么需要 实现 Callable接口
4-10 join线程
join:等待线程执行完毕
/**
* join:等待线程完成
*/
public class ThreadJoin extends Thread{
//重写Thread中的run方法
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 50; i++) {
System.out.println(this.getName()+"----i---"+i);
}
}
/**
* 主线程 main方法
*/
public static void main(String[] args) {
ThreadJoin thread = new ThreadJoin();
thread.setName("线程一");
//让线程就绪
thread.start();
try {
for (int j = 0; j < 50; j++) {
if(j==20) {
//当 j 等于 20的时候,让 thread执行,知道thread线程执行完毕之后,才把机会交给其他线程
thread.join();
}
System.out.println(Thread.currentThread().getName()+"----j---"+j);
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
4-11模拟网上取钱
- 该类用于封装账号相关信息
public class Account {
//卡号
private String cardId;
//密码
private String password;
//余额
private double amount;
//定义取钱的方法
public void draw(double drawMoney) {
//判断余额是否大于用户取款金额
if(this.getAmount() >= drawMoney) {
try {
//让线程睡眠
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
//更新余额
this.setAmount(this.getAmount() - drawMoney);
System.out.println(Thread.currentThread().getName()+"取款成功,卡中还有余额"+this.getAmount());
}else {
System.out.println(Thread.currentThread().getName()+"取款失败,余额不足!");
}
}
}
- 模拟线程
通过创建DrawThread 2个实例 模拟小明 和 小明老婆 两个线程
lic class DrawThread extends Thread {
//定义账户信息
private Account account;
//定义取款金额
private double drawMoney;
public DrawThread(Account account,double drawMoney) {
// TODO Auto-generated constructor stub
this.account = account;
this.drawMoney = drawMoney;
}
//重写Thread类的run方法,
@Override
public void run() {
// TODO Auto-generated method stub
//调用draw方法进行取钱
account.draw(drawMoney);
}
}
- 程序入口:使用同一个账号资源
public class MainTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建账户
Account account = new Account();
System.out.println("account:"+account);
//这是余额
account.setAmount(10000);
//模拟小明以及小明老婆两个取钱的线程 第一个参数:取款账户信息 第二个参数:取多少钱
DrawThread thread01 = new DrawThread(account,1000);
//设置线程名字
thread01.setName("小明");
//第一个参数:取款账户信息 第二个参数:取多少钱
DrawThread thread02 = new DrawThread(account,1000);
//设置线程名字
thread02.setName("小明老婆");
//让两个线程就绪,准备取钱,cpu会调用thread01的run方法
thread01.start();
thread02.start();
}
}
- 程序人口 使用不同的资源,及操作不同的账户
public class MainTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建账户
Account account = new Account();
System.out.println("account:"+account);
//这是余额
account.setAmount(10000);
//模拟小明以及小明老婆两个取钱的线程 第一个参数:取款账户信息 第二个参数:取多少钱
DrawThread thread01 = new DrawThread(account,10000);
//设置线程名字
thread01.setName("小明");
//第一个参数:取款账户信息 第二个参数:取多少钱
DrawThread thread02 = new DrawThread(account,10000);
//设置线程名字
thread02.setName("小明老婆");
//让两个线程就绪,准备取钱,cpu会调用thread01的run方法
thread01.start();
thread02.start();
}
}
线程安全问题:
线程安全主要包含线程间同步和线程间通信
线程安全问题: 当多个线程并发修改某个竞争资源,就会导致线程安全问题。
- 线程间同步:
- synchronized:使用synchronized修饰方法
- ThreadLocal:需要同步的代码块使用ThreadLocal同步,提供同步监视器;线程局部变量,ThreadLocal会把竞争资源,针对每个线程复制一个副本, 每个线程要修改竞争资源时,其实是修改的自己有拥有的副本。
- 线程间通信:
- wait以及notify,notifyAll,
- Lock对象
4-12 synchronized 通过同步锁保证取款线程安全
通过synchronized实现线程安全:
1、方法使用synchronized修饰
2、需要同步的代码块使用synchronized同步,提供同步监视器防止多个线程同时操作统一资源;
synchronized(同步锁)修饰方法:
定义取钱的方法 用synchronized修饰方法,该方法就是线程安全的,同一时刻只有一个线程可以进入该方法,必须等该线程执行完毕之后,其他线程才能进来
通过synchronized实现线程安全:
public class Account {
//卡号
private String cardId;
//密码
private String password;
//余额
private double amount;
//定义取钱的方法 用synchronized修饰方法,该方法就是线程安全的,同一时刻只有一个线程可以进入该方法,必须等该线程执行完毕之后,其他线程才能进来
public synchronized void draw(double drawMoney) {
//判断余额是否大于用户取款金额
if(this.getAmount() >= drawMoney) {
try {
//让线程睡眠
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
//更新余额
this.setAmount(this.getAmount() - drawMoney);
System.out.println(Thread.currentThread().getName()+"取款成功,卡中还有余额"+this.getAmount());
}else {
System.out.println(Thread.currentThread().getName()+"取款失败,余额不足!");
}
}
}
- 程序入口 操作同一个资源
public class MainTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建账户
Account account = new Account();
System.out.println("account:"+account);
//这是余额
account.setAmount(10000);
//模拟小明以及小明老婆两个取钱的线程 第一个参数:取款账户信息 第二个参数:取多少钱
DrawThread thread01 = new DrawThread(account,10000);
//设置线程名字
thread01.setName("小明");
//第一个参数:取款账户信息 第二个参数:取多少钱
DrawThread thread02 = new DrawThread(account,10000);
//设置线程名字
thread02.setName("小明老婆");
//让两个线程就绪,准备取钱,cpu会调用thread01的run方法
thread01.start();
thread02.start();
}
}
- 同步代码块
public class Account {
//卡号
private String cardId;
//密码
private String password;
//余额
private double amount;
//定义取钱的方法
public void draw(double drawMoney) {
//定义同步代码块
synchronized(this) {
//判断余额是否大于用户取款金额
if(this.getAmount() >= drawMoney) {
try {
//让线程睡眠
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
//更新余额
this.setAmount(this.getAmount() - drawMoney);
System.out.println(Thread.currentThread().getName()+"取款成功,卡中还有余额"+this.getAmount());
}else {
System.out.println(Thread.currentThread().getName()+"取款失败,余额不足!");
}
}
}
}
线程同步 :ReentrantLock()
// 锁只能是同一把 只能同时一个线程用,一个线程用完 锁打开 交给下个线程来使用
通过ReentrantLock对象对资源进行加锁操作
synchronized与ReentrantLock()的不同
synchronized锁方法
ReentrantLock锁代码块
private final ReentrantLock lk = new ReentrantLock();
//上锁
lock.lock();
//需要被锁住的资源
//释放锁
lock.unlock();
不加锁可能出现的现象:
加锁:
例子:
该类用于封装账号相关信息
public class Account {
//定义ReentrantLock,对资源进行加锁
private final ReentrantLock lock = new ReentrantLock();
//卡号
private String cardId;
//密码
private String password;
//余额
private double amount;
//定义取钱的方法
public void draw(double drawMoney) {
try {
//加锁
lock.lock();
//判断余额是否大于用户取款金额
if(this.getAmount() >= drawMoney) {
try {
//让线程睡眠
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
//更新余额
this.setAmount(this.getAmount() - drawMoney);
System.out.println(Thread.currentThread().getName()+"取款成功,卡中还有余额"+this.getAmount());
}else {
System.out.println(Thread.currentThread().getName()+"取款失败,余额不足!");
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally {
//释放锁
lock.unlock();
}
}
}
线程:
/ * 模拟线程
* 通过创建DrawThread 2个实例 模拟小明 和 小明老婆 两个线程
*
*/
public class DrawThread extends Thread {
//定义账户信息
private Account account;
//定义取款金额
private double drawMoney;
public DrawThread(Account account,double drawMoney) {
// TODO Auto-generated constructor stub
this.account = account;
this.drawMoney = drawMoney;
}
//重写Thread类的run方法,
@Override
public void run() {
// TODO Auto-generated method stub
//调用draw方法进行取钱
account.draw(drawMoney);
}
}
入口类
/* 取款,但是操作不同的账户
*/
public class MainTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建账户
Account account = new Account();
System.out.println("account:"+account);
//这是余额
account.setAmount(10000);
//模拟小明以及小明老婆两个取钱的线程 第一个参数:取款账户信息 第二个参数:取多少钱
DrawThread thread01 = new DrawThread(account,10000);
//设置线程名字
thread01.setName("小明");
//第一个参数:取款账户信息 第二个参数:取多少钱
DrawThread thread02 = new DrawThread(account,10000);
//设置线程名字
thread02.setName("小明老婆");
//让两个线程就绪,准备取钱,cpu会调用thread01的run方法
thread01.start();
thread02.start();
}
}
4-14线程同步:ThreadLocal讲解
ThreadLocal也与线程安全有关,当有多个线程同时竞争同一个资源的时候,会为每一个线程克隆一个资源的副本,每个线程操作的都是资源的副本,每个线程之间互不烦扰;
public class ThreadTest extends Thread{
private SequenceNum sequenceNum;
public ThreadTest(SequenceNum sequenceNum) {
super();
// TODO Auto-generated constructor stub
this.sequenceNum = sequenceNum;
}
@SneakyThrows
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 3; i++) {
Thread.sleep(2000);
System.out.println("当前线程名:"+this.getName()+" ---i:"+sequenceNum.getNextNum());
}
}
}
- 三个线程都在操作同一份资源(i)
public class SequenceNum {
/*
*在没有使用ThreadLocal的时候,三个线程都在操作同一份资源(i)
* */
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
//定义静态成员变量
static Integer i;
//定义实例方法,每次调用该实例方法 i的值都会自增
public Integer getNextNum() {
if(i == null) {
i = 0;
}
//i自增
i++;
return i;
}
public static void main(String[] args) {
//创建SequenceNum实例
SequenceNum sequenceNum = new SequenceNum();
//模拟多个线程
ThreadTest threadTest01 = new ThreadTest(sequenceNum);
threadTest01.setName("线程1");
ThreadTest threadTest02 = new ThreadTest(sequenceNum);
threadTest02.setName("线程2");
ThreadTest threadTest03 = new ThreadTest(sequenceNum);
threadTest03.setName("线程3");
//让3个线程就绪
threadTest01.start();
threadTest02.start();
threadTest03.start();
}
}
- 如果希望让三个线程操作各自的副本,类似克隆 3个i出来,可以通过java提供的类ThreadLocal来实现
通过ThreadLocal让每个线程操作自己独立的副本,在没有使用ThreadLocal的时候,三个线程都在操作同一份资源(i)
public class SequenceNum {
/*
* 通过ThreadLocal让每个线程操作自己独立的副本,在没有使用ThreadLocal的时候,三个线程都在操作同一份资源(i)
* 如果希望让三个线程操作各自的副本,类似克隆 3个i出来,可以通过java提供的类ThreadLocal来实现
* */
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
//定义静态成员变量
static Integer i;
//定义实例方法,每次调用该实例方法 i的值都会自增
public Integer getNextNum() {
//从 ThreadLocal 中 获取每个线程各自的副本
i = threadLocal.get();
//因为从没存过,第一次取不到为null
if(i == null) {
i = 0;
}
//i自增
i++;
//将i的值存放在 threadLocal 中
threadLocal.set(i);
return i;
}
public static void main(String[] args) {
//创建SequenceNum实例
SequenceNum sequenceNum = new SequenceNum();
//模拟多个线程
ThreadTest threadTest01 = new ThreadTest(sequenceNum);
threadTest01.setName("线程1");
ThreadTest threadTest02 = new ThreadTest(sequenceNum);
threadTest02.setName("线程2");
ThreadTest threadTest03 = new ThreadTest(sequenceNum);
threadTest03.setName("线程3");
//让3个线程就绪
threadTest01.start();
threadTest02.start();
threadTest03.start();
}
}
ThreadLocal使用场景
线程间通信 wait以,notify,notifyAll
4-16 线程间通信 :wait以及notify,notifyAll
通过Object中wait以及notify等方法实现线程通信
★- wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。该wait()方法有三种形式:无时间参数的wait(一直等待,直到其他线程通知),带毫秒参数的wait和带毫秒、微秒参数的wait(这两种方法都是等待指定时间后自动苏醒)。调用wait()方法的当前线程会释放对该同步监视器的锁定。
★- notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。
★- notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
★必须要把wait和notify放到同步代码块或者同步的方法里面。
public class Account {
//卡号
private String cardId;
//密码
private String password;
//余额
private double balance;
//定义标识符 ,用于记录卡里是否有钱
private boolean flag = true;
//取钱
public synchronized void drawMoney(double quKuanMoney) {
try {
if(flag) {
if(this.getBalance() >= quKuanMoney) {
//更新余额
this.setBalance(this.getBalance() - quKuanMoney);
System.out.println(Thread.currentThread().getName()+" 取款成功,余额"+this.getBalance());
//声明卡中没钱了
flag = false;
//通知其他线程
this.notifyAll();
//当前线程处于等待状态
this.wait();
}else {
//余额不够
System.out.println(Thread.currentThread().getName()+" 取款失败,余额不够!");
//通知其他线程
this.notifyAll();
//当前线程处于等待状态
this.wait();
}
}else {
//说明没钱,通过 小头爸爸 二叔 王叔,给小明打钱
//通知其他线程
this.notifyAll();
//当前线程处于等待状态
this.wait();
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
//存钱
public synchronized void saveMoney(double saveMoney) {
try {
//睡眠一秒钟
Thread.sleep(1000);
if(flag) {
//卡里有钱,通知小明取钱
//通知其他线程
this.notifyAll();
//当前线程处于等待状态
this.wait();
}else {
//卡里没钱,直接更新卡中余额
this.setBalance(this.getBalance() + saveMoney);
System.out.println(Thread.currentThread().getName()+" 存款成功,余额"+this.getBalance());
//将标志位改成 true,相当于声明卡中有钱
flag = true;
//通知其他线程,包括小明
this.notifyAll();
//当前线程处于等待状态
this.wait();
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
模拟取钱线程
/* 模拟取钱线程
*/
public class DrawThread extends Thread {
//存款账户
private Account account;
//存款金额
private double quKuanMoney;
public DrawThread(Account account,double quKuanMoney) {
this.account = account;
this.quKuanMoney = quKuanMoney;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
//开始取钱
account.drawMoney(quKuanMoney);
}
}
}
模拟存钱线程
/* 模拟存钱线程
*/
public class SaveThread extends Thread {
//存款账户
private Account account;
//存多少钱
private double saveMoney;
public SaveThread(Account account,double saveMoney) {
this.account = account;
this.saveMoney = saveMoney;
}
@Override
public void run() {
// TODO Auto-generated method stub
//不断给小明打钱
while(true) {
//给指定账户存钱
account.saveMoney(saveMoney);
}
}
}
/*
* 启动线程进行存款以及取款操作:
* 1、小明负责取钱
* 2、小明老爸、小明二叔、隔壁王叔 负责给小明账户存钱
* 3、当小明的取款金额大于余额就可以取钱,当余额不够就通知其他线程给小明账户存钱
* 存完钱之后就通知小明去取钱
* 那么需要用到
* wait方法 以及 notify、notifyAll进行线程通信
*/
public class ThreadTest {
public static void main(String[] args) {
//创建账户
Account account = new Account();
//卡号
account.setCardId("88888");
//密码
account.setPassword("1234");
//设置余额
account.setBalance(10000);
//模拟四个线程 一个负责取钱 三个负责存钱 第一个参数:账户 第二个参数:取款金额
DrawThread drawThread = new DrawThread(account,10000);
drawThread.setName("大头儿子");
//让线程就绪
drawThread.start();
/**三个负责存钱, 第一个参数:账户 第二个参数:取款金额*/
// 第一个参数:账户 第二个参数:存款金额
SaveThread saveThread01 = new SaveThread(account,10000);
saveThread01.setName("小头爸爸");
// 第一个参数:账户 第二个参数:存款金额
SaveThread saveThread02 = new SaveThread(account,10000);
saveThread02.setName("二叔");
// 第一个参数:账户 第二个参数:存款金额
SaveThread saveThread03 = new SaveThread(account,10000);
saveThread03.setName("隔壁老王");
//让线程就绪
saveThread01.start();
saveThread02.start();
saveThread03.start();
}
}
4-17线程池
线程池优点:
1.减少线程间切换系统运行内存的开销
★ 系统启动一个新线程的成本是比较高的,因为它涉及到与操作系统交互。在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当run方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run方法。
★ JDK 1.5创建线程池非常容易:
JDK 1.5提供了ExecutorService对象来代表线程池。
JDK1.5提供了一个Executors工厂类来产生线程池,该工厂类里包含如下几个静态工厂方法来创建线程池:
- newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
- newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。
- newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于newFixedThreadPool方法时传入参数为1。
- newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。 corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。
- newSingleThreadScheduledExecutor():创建只有一条线程的线程池,它可以在指定延迟后执行线程任务。
★ 在JDK 1.5时,使用线程池的方法:
(1)先调用Executors工厂类的static方法,创建ExecutorService对象,可作为线程池。
(2)将一个Runnable对象,或Callable对象的提交给ExecutorService线程池。
submit() - 让线程池尽早执行该Runnable对象。
schedule() - 让线程池在暂停某个时间后执行Runnable对象。
(3)调用线程池的方法关闭 ,不调用 executorService.shutdown(); 程序不会运行结束
/* 线程池
*/
public class ThreadPoolTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
//通过Executors的静态方法获取一个线程池 newFixedThreadPool(3):创建容量为3的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
//创建Runnable实现类ThreadTest的实例
ThreadTest threadTest = new ThreadTest();
//将Runnable实现类ThreadTest的实例提交给线程池,线程池会将 threadTest包装成一个线程,放在池子中,并且会通知CPU调用run方法
//创建一个线程放在线程池,并执行 run方法
executorService.submit(threadTest);
//创建一个线程放在线程池,并执行 run方法
executorService.submit(threadTest);
//创建一个线程放在线程池,并执行 run方法
executorService.submit(threadTest);
//从池子中拿出一个线程并执行
executorService.submit(threadTest);
//关闭线程池
executorService.shutdown();
}
}
class ThreadTest implements Runnable{
@SneakyThrows
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 3; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"---i:"+i);
}
System.out.println("------------"+Thread.currentThread().getName()+"执行结束"+"------------");
}
}
pool-1-thread-2---i:0
pool-1-thread-3---i:0
pool-1-thread-1---i:0
pool-1-thread-1---i:1
pool-1-thread-2---i:1
pool-1-thread-3---i:1
pool-1-thread-3---i:2
pool-1-thread-2---i:2
------------pool-1-thread-2执行结束------------
------------pool-1-thread-3执行结束------------
pool-1-thread-1---i:2
------------pool-1-thread-1执行结束------------
pool-1-thread-2---i:0
pool-1-thread-2---i:1
pool-1-thread-2---i:2
------------pool-1-thread-2执行结束------------
- 创建池子容量为1的线程池,往里面放3个线程的
//通过Executors的静态方法获取一个线程池 newFixedThreadPool(1):创建容量为1的线程池
ExecutorService executorService = Executors.newFixedThreadPool(1);
/* 线程池
*/
public class ThreadPoolTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
//通过Executors的静态方法获取一个线程池 newFixedThreadPool(3):创建容量为3的线程池
ExecutorService executorService = Executors.newFixedThreadPool(1);
//创建Runnable实现类ThreadTest的实例
ThreadTest threadTest = new ThreadTest();
//将Runnable实现类ThreadTest的实例提交给线程池,线程池会将 threadTest包装成一个线程,放在池子中,并且会通知CPU调用run方法
//创建一个线程放在线程池,并执行 run方法
executorService.submit(threadTest);
//创建一个线程放在线程池,并执行 run方法
executorService.submit(threadTest);
//创建一个线程放在线程池,并执行 run方法
executorService.submit(threadTest);
//从池子中拿出一个线程并执行
executorService.submit(threadTest);
//关闭线程池
executorService.shutdown();
}
}
class ThreadTest implements Runnable{
@SneakyThrows
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 3; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"---i:"+i);
}
System.out.println("------------"+Thread.currentThread().getName()+"执行结束"+"------------");
}
}
结果:
就体现出线程池优点:.减少线程间切换系统运行内存的开销
pool-1-thread-1---i:0
pool-1-thread-1---i:1
pool-1-thread-1---i:2
------------pool-1-thread-1执行结束------------
pool-1-thread-1---i:0
pool-1-thread-1---i:1
pool-1-thread-1---i:2
------------pool-1-thread-1执行结束------------
pool-1-thread-1---i:0
pool-1-thread-1---i:1
pool-1-thread-1---i:2
------------pool-1-thread-1执行结束------------
pool-1-thread-1---i:0
pool-1-thread-1---i:1
pool-1-thread-1---i:2
------------pool-1-thread-1执行结束------------
4-18线程组(用的不多)
线程组:
Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序 直接对线程组进行控制。
一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组。
/**
* 线程组:可以对线程进行批量管理,包括可以用线程组处理该组中所有的线程出现的异常
*/
public class ThreadGroupTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建线程组
ThreadGroup threadGroup = new ThreadGroup("线程组一") {
@Override
public void uncaughtException(Thread t, Throwable e) {
// TODO Auto-generated method stub
//e.printStackTrace();
System.out.println(t.getName()+" 异常的原因:"+e.getMessage());
}
};
//创建线程
Th th1 = new Th();
//创建线程 指定该线程属于哪一个线程组 第一个参数:线程组 第二个参数:Runnable实现类实例 第三个参数:线程名字
Thread thread = new Thread(threadGroup,th1,"线程一");
//让线程就绪
thread.start();
Thread thread2 = new Thread(threadGroup,th1,"线程二");
thread2.start();
System.out.println("====代码执行完毕===");
}
}
class Th implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 20; i++) {
if(i==10) {
System.out.println(20/(i-10));
}else {
System.out.println("线程名:"+Thread.currentThread().getName()+" i:"+i);
}
}
System.out.println(Thread.currentThread().getName()+"执行完毕=====");
}
}
4-20后台线程|守护线程
/* 线程中常用方法讲解
*/
public class ThreadTest extends Thread{
/* (non-Javadoc)
* @see java.lang.Thread#run()
*/
@SneakyThrows
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 5; i++) {
Thread.sleep(100);
//获取当前线程实例
Thread thread = Thread.currentThread();
//获取线程的名字,可以重复
String name = thread.getName();
//获取线程id,线程id是不会重复的,好比人的省份证一样不会重写
long id = thread.getId();
//获取线程的优先级 如果线程的优先级越大,说明获取CPU的执行机会越大
int priority = thread.getPriority();
System.out.println("name:"+name+" id:"+id +" 优先级:"+priority);
}
}
/**
* @param args
*/
public static void main(String[] args) throws InterruptedException {
ThreadTest thread = new ThreadTest();
thread.setName("线程一");
//设置线程的优先级
thread.setPriority(Thread.MIN_PRIORITY);
//设置为后台线程 最后死亡,最后死亡不代表最后执行完毕
thread.setDaemon(true);
//让线程就绪
thread.start();
for (int i = 0; i < 5; i++) {
Thread.sleep(100);
//获取当前线程实例
Thread thread2 = Thread.currentThread();
//获取线程的名字,可以重复
String name = thread2.getName();
//获取线程id,线程id是不会重复的,好比人的省份证一样不会重写
long id = thread2.getId();
//获取线程的优先级 如果线程的优先级越大,说明获取CPU的执行机会越大
int priority = thread2.getPriority();
System.out.println("name:"+name+" id:"+id +" 优先级:"+priority);
}
}
}
后台线程最后死亡,但并不最后执行
4-21通过Collections保证集合线程安全
- java中提供了一个工具类 Collections,调用Collections类中指定的方法可以获取线程安全的集合
例如:ArrayList<>() 是线程不安全的集合,放入Collections.synchronizedList(new ArrayList())中返回的集合就是线程安全的
public static void main(String[] args) {
// TODO Auto-generated method stub
//java中提供了一个工具类 Collections,调用Collections类中指定的方法可以获取线程安全的集合
List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
//获取线程安全的HashMap集合
Map<Integer,String> maps = Collections.synchronizedMap(new HashMap<Integer,String>());
}