文章目录
票务中心案例
票 100
售票途径 (多线程)
// 票务中心
public class Ticket implements Runnable{
// 票的数量为 100 张
int count = 100;
@Override
public void run() {
// 循环卖票
while (true){
if(count > 0){
// 为了让问题更加突显
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t第"+count+"张");
}else{
break;
}
// 每循环一次, 总票数 -1
count--;
}
}
}
测试类
public class TicketDemo {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t,"网络售票");
Thread t2 = new Thread(t,"售票窗口");
Thread t3 = new Thread(t,"黄牛党");
t1.start();
t2.start();
t3.start();
}
}
运行结果分析
- 重复票 一张票卖出两次
- 卖票顺序不同
- 负数票
产生该问题的原因
CPU 选择线程的随机性
思考解决方式
在容易出现问题的代码上 上锁
- 容易出现问题的代码
一个售票员 每卖出一张票, 必须重新设置该票的总数, 中间的过程不允许被其它售票员强行中断
一旦中断, 很容易发生 线程安全问题
代码锁的格式
同步代码锁
synchronized (锁对象){
// 容易发生问题的代码
}
对锁对象的要求是, 多个线程的锁对象必须是同一个
代码锁演示
@Override
public void run() {
while(true){
synchronized (this){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(count <= 0){
break;
}
System.out.println(Thread.currentThread().getName()+"\t第"+count+"张");
count--;
}
}
}
同步方法锁
成员方法
public void run() {
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
sellTicket();
}
}
public synchronized void sellTicket(){
if(count <= 0){
return;
}
System.out.println(Thread.currentThread().getName()+"\t第"+count+"张");
count--;
}
synchronized 存在一定有锁对象存在
成员方法 的锁对象为 this
静态方法
只是把成员方法 添加静态修饰符
静态方法 的锁对象为 该类的字节码对象
使用同步代码锁优劣分析
优势
- 可以避免线程安全问题的出现
劣势
- 必须等待一个线程运行完毕同步代码块 之后, 其他线程才可以运行
导致效率变低
速度和安全 对立的双方
对比之前的对象
ArrayList 和 Vector synchronized
StringBuffer (synchronized) 和 StringBuilder
学习完同步之后再看线程状态
同步代码锁, 把一块区域的代码 加锁, 一次只能允许一条线程 进入加锁的代码
提款机, 从一个人进入提款机面前, 插卡, 输入密码 , 输入提款金额, 把钱放进钱包, 退卡
其他线程 在排队等待 即使CPU 把执行权给了 线程, 但是该线程也无法进入上锁区域
lock锁
lock 锁基本介绍
Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition 。
锁是用于通过多个线程控制对共享资源的访问的工具。 通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁
基本使用语法
public void sellTicket(){
// 获得锁。
lock.lock();
if(count <= 0){
return;
}
System.out.println(Thread.currentThread().getName()+"\t第"+count+"张");
count--;
// 释放锁
lock.unlock();
}
问题分析
万一在 lock() 和 unlock() 之间 出现了 异常
将不会执行 unlock() 释放锁
代码优化方式
public void sellTicket(){
// 获得锁。
lock.lock();
try{
if(count <= 0){
return;
}
System.out.println(Thread.currentThread().getName()+"\t第"+count+"张");
count--;
}finally{
// 释放锁
lock.unlock();
}
}
和 synchronized 对比
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()放到finally{} 中;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
死锁问题
// 场景模拟
张三 和 李四
public class MyThread extends Thread{
public static final Object left = new Object();
public static final Object right = new Object();
public boolean boo ;
public MyThread(){}
public MyThread(boolean boo) {
this.boo = boo;
}
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// true 张三开始从左向右走
if(boo){
while (true){
leftRight();
}
}else{
// false 李四开始从右向左走
while(true){
rightLeft();
}
}
}
public void leftRight(){
synchronized (left){
System.out.println("张三进入了左侧房间!");
synchronized (right){
System.out.println("张三进入了右侧房间!");
}
}
}
public void rightLeft(){
synchronized (right){
System.out.println("李四进入了右侧房间!");
synchronized (left){
System.out.println("李四进入了左侧房间!");
}
}
}
}
测试
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread(true);
t1.start();
MyThread t2 = new MyThread(false);
t2.start();
}
}
线程池
线程的生命周期
用户使用线程的步骤为
创建线程 => 启动线程 => 可运行=运行 => 阻塞或销毁.....
把线程看做是一辆自行车
用户 想要从家去公司, 想要一辆自行车
买自行车 => 骑自行车 => 销毁自行车
程序的运行结束 是需要销毁已经创建的线程对象的, 根据自行车的案例, 现实生活中有共享单车, 可以很好地优化线程的获取方式
租车 => 用车 => 还车
租用线程 => 使用线程 => 归还线程
线程池
Executor
Interface Executor
界面提供了一种将任务提交从每个任务的运行机制分解的方式,包括线程使用,调度等的Executor
Executors
static ThreadFactory defaultThreadFactory()
返回用于创建新线程的默认线程工厂。
static ExecutorService newCachedThreadPool()
创建一个根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程。
static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
创建一个根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程,并在需要时使用提供的ThreadFactory创建新线程。
static ExecutorService newFixedThreadPool(int nThreads)
创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程。
static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
创建一个线程池,重用固定数量的线程,从共享无界队列中运行,使用提供的ThreadFactory在需要时创建新线程。
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个线程池,可以调度命令在给定的延迟之后运行,或定期执行。
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
创建一个线程池,可以调度命令在给定的延迟之后运行,或定期执行。
案例
public class Demo1_Pool {
public static void main(String[] args) {
Ticket ticket = new Ticket();
// 线程池本质上就是一个容器, 该容器存放的内容是 线程
// 用户使用线程, 从该容器中自动获取
ExecutorService pool = Executors.newCachedThreadPool();
pool.submit(ticket);
pool.submit(ticket);
pool.submit(ticket);
}
}
优势
- 不需要创建线程类 , 直接使用线程池, 实现业务逻辑和线程分离
一个人 开了一个 淘宝网店卖零食, 需要组建自己的快递团队吗?
人负责好自己的主业就可以了, 不要在快递上分心, 直接把业务打包给快递公司负责
需要很多辆自行车, 直接找共享单车合作
线程池就是属于第三方公司
- 使用线程池, 可以节约资源, 提高系统效率
同一辆车 可以提供给多人重复使用
线程之间的通讯问题
之前的卖票案例中 多个线程共同争抢CPU资源
但是线程之间不存在依赖关系
两个卖票窗口之间是相互独立的, 虽然有共享资源存在, 两者之间不存在先后关系!
通讯问题引入
存在依赖关系的两个线程之间 比如超市案例 共享同一个超市共享资源的两个线程 , 供货商线程 消费者线程, 如果供货商没有供货,消费者将无法消费, 消费者线程依赖于 供货商线程, 一旦存在依赖关系, 需要线程之间进行通讯
案例资源
共享资源类
public class SharedData {
private char c;
// 是否存在 已经生产完成的字符
private boolean isProduced = false;
// 生产字一个符的方法
public void putShareChar(char c){
this.c = c;
// 修改状态
isProduced = true;
System.out.println("生产了一个字符! "+c);
}
public char getShareChar(){
// 调用该方法, 是从共享数据中 拿取一个字符
// 把字符取出之后, 修改状态
isProduced = false;
System.out.println("消费了一个字符"+c);
return c;
}
}
生产者类
public class Producer extends Thread{
private SharedData sd;
public Producer(SharedData sd) {
this.sd = sd;
}
@Override
public void run() {
// 生产者 循环 向仓库 放入字符, 生产商品放入仓库
for(char ch = 'A' ; ch <= 'D' ; ch++){
// 时间差
try {
Thread.sleep((long)(Math.random()*3000));
} catch (InterruptedException e) {
e.printStackTrace();
}
sd.putShareChar(ch);
}
}
}
消费者类
public class Consumer extends Thread{
private SharedData sd;
public Consumer(SharedData sd) {
this.sd = sd;
}
@Override
public void run() {
// 消费者去买东西
char ch;
do{
// 时间差
try {
Thread.sleep((long)(Math.random()*3000));
} catch (InterruptedException e) {
e.printStackTrace();
}
ch = sd.getShareChar();
}while(ch != 'D');
}
}
测试类
public class Demo {
public static void main(String[] args) {
// 共享资源对象
SharedData sd = new SharedData();
// 生产者线程
Producer pro = new Producer(sd);
Consumer con = new Consumer(sd);
pro.start();
con.start();
}
}
没有同步, 没线程之间也没有通讯
生产者还未生产, 消费者就已经开始消费了
案例优化
public class SharedData {
private char c;
// 是否存在 已经生产完成的字符
private boolean isProduced = false;
// 生产字一个符的方法
public synchronized void putShareChar(char c) {
if(isProduced){
//
System.out.println("仓库的商品 还未销售完毕, 生产者停止生产");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.c = c;
// 修改状态
isProduced = true;
System.out.println("生产了一个字符! "+c);
this.notify();
}
public synchronized char getShareChar(){
// 只有当 产品有的时候, 才可以买,
if(!isProduced){
// 没有的时候只能等
System.out.println("生产者还未生产完毕, 请等待");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 调用该方法, 是从共享数据中 拿取一个字符
// 把字符取出之后, 修改状态
isProduced = false;
System.out.println("消费了一个字符"+c);
this.notify();
return c;
}
}
wait 和 notify
wait 等待 挂起
需要被唤醒
notify() 唤醒的方法 可以唤醒正在等待的线程
实际上是属于 Object 类的两个方法
调用两个方法的主体是 共享资源对象, 因为任何类都可以作为共享资源而存在
wait() 和 sleep() 的区别
wait() 在 线程暂停执行之后 会释放锁
sleep() 在线程暂停执行之后, 不会释放锁
类似于 银行提款机,张三在提款机中取钱, 突发疾病 在提款机旁 晕倒
wait() 在晕倒之前把门打开
sleep() 直接晕倒
线程和IO流的综合案例
使用原始方式复制多文件
package s0805;
import java.io.*;
public class Demo1_copy {
/*
// 1- 判断 目标文件夹是否存在, 不存在创建
// 2- 创建 源文件 输入流对象
// 3- 创建 目标文件 输出流对象
// 4- 使用包装流 buffered
// 5- 使用 for循环 多次进行文件复制
// 6- 关闭资源
*/
public void copyFiles(String targetDir,String ...files) throws Exception {
// 1- 判断 目标文件夹是否存在, 不存在创建
File dir = new File(targetDir);
if(!dir.exists()){
dir.mkdirs();
}
// 2- 创建 源文件 输入流对象
// 使用 for循环
for(String file : files){
File sourceFile = new File(file);
FileInputStream fis = new FileInputStream(sourceFile);
// 3- 创建 目标文件 输出流对象
File targetFile = new File(targetDir,sourceFile.getName());
FileOutputStream fos = new FileOutputStream(targetFile);
// 4- 使用包装流 buffered
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
// 5- 多次进行文件复制
byte [] bys = new byte[1024];
int len;
// 文件开始复制之前 打印一句话
System.out.println(sourceFile.getName()+"开始复制");
while( (len = bis.read(bys)) != -1){
bos.write(bys,0,len);
}
System.out.println(sourceFile.getName()+"复制完成");
// 6- 关闭资源
bis.close();
bos.close();
}
}
}
测试类
public class Demo2_copytest {
public static void main(String[] args) throws Exception {
Demo1_copy copy = new Demo1_copy();
String dir = "D:\\testCopy";
String f1 = "D:\\55- java 软件\\Mysql\\mysql-installer-community-5.7.19.0.msi";
String f2 = "D:\\55- java 软件\\Mysql\\Navicat for MySQL.rar";
String f3 = "D:\\55- java 软件\\IDEA\\ideaIU-2017.2.6.exe";
copy.copyFiles(dir,f1,f2,f3);
}
}
使用多线程 完成多文件复制案例
需求分析
// 1- 新建线程类, 该类主要负责 一个文件的复制工作
// 2- 线程类需要两个属性, 分别表示 源文件, 和目标文件夹
// 3- 在run 方法中 把文件复制的代码 补充完毕, (注意必须内部处理异常)
// 4- 在测试类中, 多文件复制, 每一个文件都需要开启一条线程
// 5- 使用 线程池来优化 线程的逻辑和效率
线程类
package s0805.copyThread;
import java.io.*;
/**
* 该线程 主要功能是 每一条线程 负责一个文件的复制拷贝工作
*/
public class CopyRunnable implements Runnable{
// 源文件
private String file;
// 目标文件夹
private String targetDir;
public CopyRunnable(){}
public CopyRunnable(String file, String targetDir) {
this.file = file;
this.targetDir = targetDir;
}
@Override
public void run() {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try{
File sourceFile = new File(file);
FileInputStream fis = new FileInputStream(sourceFile);
// 3- 创建 目标文件 输出流对象
File targetFile = new File(targetDir,sourceFile.getName());
FileOutputStream fos = new FileOutputStream(targetFile);
// 4- 使用包装流 buffered
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);
// 5- 多次进行文件复制
byte [] bys = new byte[1024];
int len;
// 文件开始复制之前 打印一句话
System.out.println(sourceFile.getName()+"开始复制");
while( (len = bis.read(bys)) != -1){
bos.write(bys,0,len);
}
System.out.println(sourceFile.getName()+"复制完成");
}catch(Exception e){
e.printStackTrace();
}finally{
// 6- 关闭资源
if(bis != null){
try {
bis.close();
} catch (IOException e) {
bis = null;
}
}
if(bos != null){
try {
bos.close();
} catch (IOException e) {
bos = null;
}
}
}
}
}
文件复制类
public void copyFiles(String targetDir,String ...files) throws Exception {
// 1- 判断 目标文件夹是否存在, 不存在创建
File dir = new File(targetDir);
if(!dir.exists()){
dir.mkdirs();
}
// 2- 创建 源文件 输入流对象
// 使用 for循环
for(String file : files){
// 1- 创建线程对象
CopyRunnable copyRunnable = new CopyRunnable(file, targetDir);
// 2- 使用线程池 来执行多条线程
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(copyRunnable);
}
}
测试类不变