【JavaSE 第十九天】
一、生产者与消费者(线程通信)
1. 安全问题产生(多生产多消费问题)
- 线程本身就只是一个新创建的方法栈内存 (CPU 进来读取数据)`
- 线程的
notify()
唤醒方法,唤醒第一个等待的线程(并不能指定唤醒)- 解决办法:全部唤醒
notifyAll()
- 解决办法:全部唤醒
- 被唤醒线程,已经进行过 if 判断,一旦醒来直接继续执行
- 解决办法:线程被唤醒后,不能立刻就执行,必须再次判断标志位,利用循环解决问题
while(标志位)
标志位是 true,永远也出不去循环,一直要等待
/**
* 定义资源对象
* 两个成员:①产生商品的计数器
* ②标志位
*/
public class Resource {
private int count ;
private boolean flag ;
// 消费者调用
public synchronized void getCount() {
// flag 是 false,消费完成,等待生产
while (!flag)
// 无限等待
try{this.wait();}catch (Exception ex){}
System.out.println("消费第"+count);
// 修改标志位,为消费完成
flag = false;
// 唤醒对方线程
this.notifyAll();
}
// 生产者调用
public synchronized void setCount() {
// flag 是 true,生产完成,等待消费
while (flag)
// 无限等待
try{this.wait();}catch (Exception ex){}
count++;
System.out.println("生产第"+count+"个");
// 修改标志位,为生产完成
flag = true;
// 唤醒对方线程
this.notifyAll();
}
}
生产者线程:
/**
* 生产者线程
* 资源对象中的 变量++
*/
public class Produce implements Runnable{
private Resource r;
public Produce(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.setCount();
}
}
}
消费者线程:
/**
* 消费者线程
* 资源对象中的变量输出打印
*/
public class Customer implements Runnable{
private Resource r;
public Customer(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.getCount();
}
}
}
调用:
public static void main(String[] args) {
Resource r = new Resource();
// 接口实现类,生产的,消费的
Produce produce = new Produce(r);
Customer customer = new Customer(r);
// 创建多个线程
new Thread(produce).start();
new Thread(produce).start();
new Thread(produce).start();
new Thread(customer).start();
new Thread(customer).start();
new Thread(customer).start();
}
(但是如此操作性能低下)
2. 线程方法 sleep()
和 wait()
的区别
sleep()
在休眠的过程中,同步锁不会丢失,不释放wait()
等待的时候,发布对象锁(监视器)的所属权,释放锁,被唤醒后要重新获取锁,才能执行
3. 生产者和消费者案例性能问题
wait()
方法和notify()
方法,是本地方法,它需要调用 OS(操作系统) 的功能和操作系统交互,JVM “找到” OS,把线程停止,如果频繁进行等方法待与唤醒,导致 JVM 和 OS 交互的次数过多,拖慢性能notifyAll()
唤醒全部的线程,也浪费线程资源,为了一个线程,不得以唤醒的了全部的线程
因此引出深入学习 Lock 接口:
4. Lock 接口深入
Lock 接口替换了同步 synchronized,提供了更加灵活,性能更好的锁定操作
- Lock 接口中方法 :
newCondition()
方法的返回值是接口:Condition
伪代码:
Lock lock = new ReentrantLock(); // Lock 接口的实现类对象
Condition con01 = lock.newCondition(); // 返回 Condition 接口实现类的对象
Condition con02 = lock.newCondition(); // 返回 Condition 接口实现类的对象
- newCondition 是一个集合容器(单列集合)(专业术语叫做:线程的阻塞队列;特点:释放锁,进入集合),并且集合具有名字,里面存储的是线程
- 这种方法的操作将线程装入集合并去掉它的锁(和操作系统失去了联系)
- newCondition 的核心在于一把同步锁可以创建两个或者多个线程的阻塞队列集合,可以让锁上的线程指定进入或出某一个集合
- 这样线程可以分集合进行管理,提高运行性能
5. 生产者与消费者改进为 Lock 接口
- Condition 接口 (线程的阻塞队列)
- 进入队列的线程,需要释放锁
- 出去队列的线程,需要再次的获取锁
- 接口的方法:
await()
线程释放锁,进入队列 - 接口的方法:
signal()
线程出队列,再次获取锁
线程的阻塞队列,必须依赖 Lock 接口创建
/**
* 改进为高性能的 Lock 接口和线程的阻塞队列
*/
public class Resource {
private int count ;
private boolean flag ;
private Lock lock = new ReentrantLock(); // Lock接口实现类对象,两个方法都要使用
// 通过 Lock 接口锁,创建出2个线程的阻塞队列
private Condition prod = lock.newCondition(); // 生产者线程阻塞队列
private Condition cust = lock.newCondition(); // 消费者线程阻塞队列
// 消费者调用
public void getCount() {
lock.lock(); // 获取锁
// flag 是 false,消费完成,等待生产
while (!flag)
// 无限等待,消费线程等待,执行到这里的线程,释放锁,进入到消费者的阻塞队列
try{cust.await();}catch (Exception ex){}
System.out.println("消费第"+count+"个");
// 修改标志位,为消费完成
flag = false;
// 唤醒生产线程队列中的一个
prod.signal();
lock.unlock(); // 释放锁
}
// 生产者调用
public void setCount() {
lock.lock(); // 获取锁
// flag 是 true,生产完成,等待消费
while (flag)
// 无限等待,释放锁,进入到生产线程队列
try{prod.await();}catch (Exception ex){}
count++;
System.out.println("生产第"+count+"个");
// 修改标志位,为生产完成
flag = true;
// 唤醒消费者线程阻塞队列中年的一个
cust.signal();
lock.unlock(); // 释放锁
}
}
再次调用…
整套操作都是 JVM 自己完成的与操作系统没有交互,所以这个操作的运行效率远高于 wait()
方法和 notify()
方法的使用操作。
6. Lock 锁的实现原理
- Lock 锁使用的技术不开源(本地方法),技术的名称叫做轻量级锁
- Lock 锁使用的是 CAS 锁 (Compare And Swap) ,(JDK5 出现)又被称为:自旋锁,属于轻量级锁
- 解释:假设两个线程进行
i++
操作,先有一个线程读取内存数据进行操作i++
,赋值之前需要判断变量的值是否被另一个线程改变,如果没有改变直接赋值,如果变量已经改变,就要再次读取变量的值…循环进行(简单来说就是:读取变量,判断变量,要么直接赋值,要么继续读取判断) 这个过程就叫做:Compare And Swap - 但是 CAS 锁容易出现一个问题:ABA 问题:即存在三个线程,当进行运算时本来就已经被提前改变变量,但第三个线程又将变量改回,所以变量被动过,但是第一个线程进行操作会出现问题。所以此时就需要加入版本号限定
- 解释:假设两个线程进行
- JDK 限制:当竞争的线程大于等于10,或者单个线程自旋超过10次的时候,JDK 强制 CAS 锁取消,直接升级为重量级锁 (OS 锁定 CPU 和内存的通信总线)
二、单例设计模式
设计模式:不是某种技术,而是以前的开发人员,为了解决某些问题实现的写代码的经验
所有的设计模式核心的技术,就是面向对象
Java 的设计模式有23种,分为3个类别:①创建型,②行为型,③功能型
1. 单例模式
要求:保证一个类的对象在内存中的唯一性
- (饿汉式)实现步骤:
- 私有修饰符修饰构造方法
- 在本类中自己创建自己的对象(这样保证不论调用多少次,只创建了一次对象,保证了对象在内存中的唯一性)
- 使用方法
get()
,返回本类对象 - 由于属于私有性质,所以无法使用对象名调用,只能用类名调用就要加上静态修饰符,方法
get()
与本类中创建对象前面用静态修饰符
/**
* 私有修饰构造方法
* 自己创建自己的对象
* 方法 get,返回本类对象
*/
public class Single {
private Single(){}
// 饿汉式(静态方法调用,速度快)
private static Single s = new Single(); // 自己创建自己的对象,还可以加入 final 修饰
// 方法 get,返回本类对象
public static Single getInstance(){
return s;
}
}
调用:
public static void main(String[] args) {
// 静态方法,获取 Single 类的对象
Single instance = Single.getInstance();
System.out.println("instance = " + instance);
}
- (懒汉式)实现步骤:
- 私有修饰构造方法
- 创建本类的成员变量,不 new 创建对象
- 方法
get()
,返回本类对象
/**
* 私有修饰构造方法
* 创建本类的成员变量,不 new 对象
* 方法 get,返回本类对象
*/
public class Single {
private Single(){}
// 由于没有一上来就创建对象,所以这种写法节省资源
// 懒汉式,对象的延迟加载
private static Single s = null;
public static Single getInstance(){
// 判断变量 s,是 null 值就创建
if (s == null) {
s = new Single();
}
return s;
}
}
2. (懒汉式)单例模式的安全问题
一个线程判断完变量
s=null
,还没有执行 new 对象,被另一个线程抢到 CPU 资源,同时有2个线程都进行判断变量,对象创建多次
改进:需要加上锁,保障了安全性
public static Single getInstance(){
synchronized (Single.class) {
// 判断变量 s,是 null 就创建
if (s == null) {
s = new Single();
}
}
return s;
}
但是影响了性能…
性能问题:第一个线程获取锁,创建对象,返回对象…都顺利进行,当第二个线程调用方法的时候,变量 s
已经有对象了,根本就不需要再进同步,不需要再判断是否为空,应该:直接 return
才是最高效的,应用双重的 if 判断,提高效率 (Double Check Lock)
改进:应用双重的 if 判断(Double Check Lock)
private static volatile Single s = null; // volatile 预防 CPU 乱序
public static Single getInstance(){
// 再次判断变量,提高效率
if(s == null) {
synchronized (Single.class) {
// 判断变量 s,是 null 就创建
if (s == null) {
s = new Single();
}
}
}
return s;
}
3. 关键字 volatile
成员变量修饰符,不能修饰其它内容
- 关键字作用:
- 保证被修饰的变量,在线程中的可见性
- 防止指令重排序(CPU 某些情况下会乱序)
- 在单例的模式中,使用了该关键字,如果不使用关键字,可能线程会拿到一个尚未初始化完成的对象(又叫做半初始化对象:内存有,地址有,里面的数据没有)
测试 volatile 的作用:
public class MyRunnable implements Runnable {
private volatile boolean flag = true;
@Override
public void run() {
m();
}
private void m(){
System.out.println("开始执行");
while (flag){ // 如果不加 volatile 修饰,这里依旧会用 CPU 中的缓存数据去执行
}
System.out.println("结束执行");
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
Thread.sleep(2000);
// main 线程修改变量的值
myRunnable.setFlag(false);
}
三、 线程池 ThreadPool
又叫做线程的缓冲池,目的就是提高效率,(
new Thread().start()
)频繁的创建线程,开线程操作(线程是内存中的一个独立的方法栈区,JVM 没有能力开辟内存空间,)这些操作需要和 OS 交互,会浪费资源
JDK5 开始内置线程池
线程池,存储线程对象,里面存储着很多个线程,需要用线程的时候,从线程池中拿出一个线程运行完之后,继续拿回线程进入线程池(保证线程“不死”),反复利用
1. Executors 类
- 静态方法
static newFixedThreadPool(int 线程的个数)
创建一个可重用固定线程的线程池- 方法的返回值是 ExecutorService 接口的实现类,ExecutorService 接口是用来管理线程池里面的线程
- ExecutorService 接口的方法
submit (Runnable r)
提交线程执行的任务
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TreadTest {
public static void main(String[] args) {
// 创建线程池,线程的个数是两个,线程池不要创建太大
ExecutorService es = Executors.newFixedThreadPool(2);
// 线程池管理对象 service,调用方法 submit 提交线程的任务
MyRunnable my=new MyRunnable();
es.submit(my); // 第一个线程
es.submit(my); // 第二个线程
es.submit(my); // 第一或二个线程,放回哪个就又重复利用
// 程序永远保持运行,线程不死
es.shutdown(); // 销毁线程池,轻易不要使用
}
}
Runnable 接口:
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 线程开始启动!");
}
}
2. Callable 接口
实现多线程的程序:接口特点是有返回值,可以抛出异常 (Runnable 没有)
Callable 接口的抽象的方法只有一个,call()
启动线程,线程调用重写方法 call()
- ExecutorService 接口的方法:
submit (Callable c)
提交线程执行的任务Future submit()
方法提交线程任务后,方法有个返回值是 Future 接口类型- Future 接口,获取到线程执行后的返回值结果
public class MyCall implements Callable<String> {
public String call() throws Exception{
return "返回字符串";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建线程池,线程的个数是 2 个
ExecutorService es = Executors.newFixedThreadPool(2);
// 线程池管理对象 service,调用方法 submit 提交线程的任务
MyRunnable my = new MyRunnable();
// 提交线程任务,使用 Callable 接口实现类
Future<String> future = es.submit(new MyCall()); // 返回接口类型 Future
// 接口的方法 get,获取线程的返回值
String str = future.get();
System.out.println("str = " + str);
}
四、 ConcurrentHashMap
ConcurrentHashMap 类本质上 Map 集合,键值对的集合,使用方式和 HashMap 没有区别,但是能够保证线程安全
凡是对于此 Map 集合的操作,只要不去修改里面的元素,它就不会锁定,如果要改动就使用 synchronized 进行上锁(一半安全,一半不安全)
五、 线程的状态图-生命周期
在某一个时刻,线程只能处于其中的一种状态(一共六种状态),这种线程的状态反映的是 JVM 中的线程状态和 OS (操作系统)无关
NEW
至今尚未启动的线程处于这种状态RUNNABLE
正在 Java 虚拟机中执行的线程处于这种状态BLOCKED
受阻塞并等待某个监视器锁的线程处于这种状态WAITING
无限期地等待另一个线程来执行某一个特定操作的线程处于这种状态TIMED_WAITING
等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态TERMINATED
已退出的线程处于这种状态
六、 File 类
- 文件夹 (Directory):存储文件的容器,为防止文件重名而设置,文件归类,文件夹本身不存储任何数据,计算专业术语称为:目录
- 文件 (File):用于存储数据,同一个目录中的文件名不能相同
- 路径 (Path):一个目录或者文件在磁盘中的准确位置
c:\jdk8\jar
是目录的路径,是个文件夹的路径(Windows 操作系统,并且不区分大小写)c:\jdk8\bin\javac.exe
是文件的路径(Windows 操作系统,并且不区分大小写)
为了方便操作,出现了 File 类
- File 类,为了描述目录文件和路径的对象,此类具有跨平台性,平台无关性
1. File 类的构造方法
File (String pathname)
传递字符串的路径名File(String parent,String child)
传递字符串的父路径,字符串的子路径File(File parent,String child)
传递 File 类型的父路径,字符串的子路径
public static void main(String[] args) {
fileMethod01()
fileMethod02()
fileMethod03();
}
/**
* File (String pathname) 传递字符串的路径名
*/
public static void fileMethod01(){
// 字符串的路径,变成 File 对象
File file = new File("C:\\Java\\jdk1.8.0_221\\bin");
System.out.println(file);
}
/**
* File(String parent,String child) 传递字符串的父路径,字符串的子路径
* C:\Java\jdk1.8.0_221\bin
* C:\Java\jdk1.8.0_221 是 C:\Java\jdk1.8.0_221\bin 的父路径(子路径可以是无限的,但是父路径必须是唯一的)
*/
public static void fileMethod02(){
String parent = "C:/Java/jdk1.8.0_221"; //或者 C:\\Java\\jdk1.8.0_221
String child = "bin";
File file = new File(parent,child);
System.out.println(file);
// 这种方法可以单独操作 父路径 或者 子路径
}
/**
* File(File parent,String child) 传递 File 类型的父路径,字符串的子路径
*/
public static void fileMethod03(){
File parent = new File("C:/Java/jdk1.8.0_221");
String child = "bin";
File file = new File(parent,child);
System.out.println(file);
}
2. File 类的创建方法
boolean createNewFile()
创建一个文件,文件路径写在 File 的构造方法中boolean mkdirs()
创建目录,目录的位置和名字写在 File 的构造方法中
// 创建文件 boolean createNewFile()
public static void fileMethod01() throws IOException {
File file = new File("C://Java//1.txt"); // 只能创建文件,但是不能往其中写东西
boolean b = file.createNewFile();
System.out.println("b = " + b);
}
// 创建文件夹(目录) boolean mkdirs()
public static void fileMethod02(){
File file = new File("C://Java//新建文件夹"); // 依旧是文件夹与文件名无关,但是绝对不可以重名,不论是什么文件格式
boolean b = file.mkdirs();
System.out.println("b = " + b);
}
3. File 类的删除方法
boolean delete()
删除指定的目录或者文件,路径写在 File 类的构造方法- 不会进入回收站,直接从磁盘中删除了,有风险
public static void fileMethod03(){
File file = new File("C:/Java/aaa"); // 如果是文件夹里面没有东西可以删除,但是如果文件夹中有内容就无法删除
boolean b = file.delete();
System.out.println("b = " + b);
}
4. File 类判断方法
boolean exists()
判断构造方法中的路径是否存在boolean isDirectory()
判断构造方法中的路径是不是文件夹boolean isFile()
判断构造方法中的路径是不是文件boolean isAbsolute()
判断构造方法中的路径是不是绝对路径
(1)绝对路径
- 绝对路径
- 在磁盘中的路径具有唯一性
- Windows 系统中,盘符开头
C:/Java/jdk1.8.0_221/bin/javac.exe
- Linux 或者 Unix 系统,
/
开头,磁盘根/usr/local
- 互联网路径:
www.baidu.com
https://www.csdn.net
(2)相对路径
- 相对路径
- 必须有参照物
C:/Java/jdk1.8.0_221/bin/javac.exe
属于绝对路径bin
是参考点:父路径C:/Java/jdk1.8.0_221
bin
是参考点:子路径javac.exe
bin
参考点:父路径使用../
表示
import java.io.File;
public class FileTest {
public static void main(String[] args) {
fileMethod01();
fileMethod02();
fileMethod03();
}
public static void fileMethod01(){
File file=new File("C:/Java/jdk1.8.0_221/bin");
boolean b= file.exists();
System.out.println("b = " + b);
}
public static void fileMethod02(){
File file=new File("C:/Java/jdk1.8.0_221/bin");
boolean b= file.isDirectory();
System.out.println("b = " + b);
}
public static void fileMethod03(){
File file=new File("C:/Java/jdk1.8.0_221/bin");
boolean b= file.isDirectory();
System.out.println("b = " + b);
}
}
/**
* boolean isAbsolute() 判断构造方法中的路径是不是绝对路径
* 不写绝对形式的路径,写相对形式的,它会默认在当前的项目(IDEA 中)路径下
*/
public static void fileMethod04(){
File file01 = new File("C:/Java/jdk1.8.0_221/bin/javac.exe");
boolean b = file01.isAbsolute();
System.out.println("b = " + b);
File file02 = new File("javac.exe"); // 相对路径
b = file02.isAbsolute();
System.out.println("b = " + b);
}