17 多线程,锁,线程池
Properties配置文件的读取
- 目的 : 便于维护
- 新建配置文件 : 在src右键–>new–>file–>config.properties
- 步骤 :
- 创建Properties对象
- 加载配置文件
- 使用对象获取配置文件中的信息
config.properties
PropertiesDemo1
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
//如何从配置文件中获取信息
public class PropertiesDemo1 {
public static void main(String[] args) throws Exception {
//1.创建properties对象
Properties pro = new Properties();
//2.通过对象加载配置文件,将配置文件中的内容加载到流中
pro.load(PropertiesDemo1.class.getClassLoader().getResourceAsStream("config.properties"));
//3.通过pro对象获取流中加载过来的数据
String value = pro.getProperty("birthday");
System.out.println(value);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date parse = sdf.parse(value);
System.out.println(parse);
}
}
标准输入输出流
- System.in : InputStream
- System.out : PrintStream
线程与进程
-
进程 ( Process ) : 一个程序的运行 , 程序在内存中分配的空间
- 至少有一个进程
-
线程 ( Thread ) : 进程中的一个执行单位 , 执行路径
- 如果进程中有多个线程—>多线程的程序
-
并行 : 某一时间点 , 有多个程序同时执行 , 多核CPU
-
并发 : 某一时间段 , 有多个程序同时执行 , 并不是真正意义的同时执行—>多线程
-
并发 ( 多线程 ) 真的是同时执行吗?
- 不是 , 而是时间间隔很短 , 造成了同时执行的错觉
-
多线程的程序有什么优点?
- 提高了用户的体验 , 提高了程序的运行效率 ? 提高了CPU的使用率
-
前面的代码都是单线程的程序 , 都是在main方法中依次执行
- 主线程 : 用于执行main方法的线程
多线程的概念
实现多线程的第一种方式
- 如何实现线程 :
- 继承Thread
- 重写run ( 线程要做的事 , 定义在该方法中 )
- 创建子类的对象
- 使用该对象调用start()方法
- run()方法和start()方法的区别 :
- run()方法 : 只是普通方法的调用 , 不会开启新的线程
- start()方法 : 开启新的线程 , 并自动调用run()方法
public class ThreadDemo1 {
public static void main(String[] args) {
MyThread th1 = new MyThread();
th1.setName("一号子线程");
th1.start();
MyThread th2 = new MyThread();
th2.setName("二号子线程");
th2.start();
Thread.currentThread().setName("主线程");
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
线程中的常用方法
- 线程阻塞
public static void sleep(long millis) //静态方法,时间结束后进入可执行
public final void join() //成员方法,被谁调用,让哪个线程先执行,执行完毕,再执行所在线程
public static void yield() //让步,让其他线程先执行,不一定生效,因为是CPU决定的
- 中断线程
public final void stop() //停止一个线程,已过时
public void interrupt() //打断线程的阻塞状态,进入可执行状态,会抛出异常
-
- sleep()方法
public class ThreadDemo2 {
public static void main(String[] args) throws Exception {
MyTheard2 mt = new MyTheard2();
Thread.sleep(3000);
mt.setName("子线程");
mt.start();
}
}
class MyTheard2 extends Thread{
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
-
- join()方法
public class ThreadDemo3 {
public static void main(String[] args) throws Exception {
MyThread3 mt3 = new MyThread3();
mt3.start();
mt3.join();
System.out.println(MyThread3.sum);
}
}
class MyThread3 extends Thread {
public static int sum = 0;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
sum += i;
}
}
}
实现多线程的第二种方式
- 实现多线程的方式二 :
- 实现Runnable接口
- 重写run()方法
- 创建Runna接口的子类对象
- 创建Thread类的对象 , 把第三步的对象传到构造方法中
- 使用Thread类的对象 , 调用start()方法
- 既然有了第一种实现方式 , 为什么还要第二种 ?
- 解决单继承的局限性
- 符合面向对象的思想 : 把线程任务和线程对象分离开来
public class RunnableDemo1 {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread th1 = new Thread(mr,"一号子线程");
Thread th2 = new Thread(mr,"二号子线程");
th1.start();
th2.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程-----"+i);
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
/**
* 创建两个线程 ,一个线程负责打印大写字母表,一个线程负责打印小写字母表
* @author Apase
*
*/
public class RunnableDemo2 {
public static void main(String[] args) {
MyRunnable1 mr1 = new MyRunnable1();
Thread th1 = new Thread(mr1);
MyRunnable2 mr2 = new MyRunnable2();
Thread th2 = new Thread(mr2);
th1.start();
th2.start();
}
}
class MyRunnable1 implements Runnable{
@Override
public void run() {
for (char i = 'a'; i <= 'z'; i++) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
class MyRunnable2 implements Runnable{
@Override
public void run() {
for (char i = 'A'; i <= 'Z'; i++) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
线程的生命周期
线程安全问题
//卖票案例
public class MyTicketsTest1 {
public static void main(String[] args) {
MyTicket mt1 = new MyTicket();
mt1.setName("北京窗口");
MyTicket mt2 = new MyTicket();
mt2.setName("西双版纳窗口");
MyTicket mt3 = new MyTicket();
mt3.setName("布宜诺斯艾利斯窗口");
mt1.start();
mt2.start();
mt3.start();
}
}
class MyTicket extends Thread{
static int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName()+"还剩"+tickets--+"张票");
} else {
break;
}
}
}
}
产生原因
在卖票过程中 , 出现了多个窗口卖同一张票 , 或者票数为0及负数的情况 , 这就是传说中的线程安全问题
- 线程安全问题产生的条件是什么 ?
- 具备多线程的环境
- 多个线程操作共享操作
- 操作共享数据的代码有多条
解决方案
- 加锁 , 让某一时刻只能有一个线程进行操作 .
- 同步代码块
- 同步方法
- Lock
同步代码块
synchronized(锁对象){
容易产生线程安全问题的代码
}
锁对象 : 可以是任意对象 , 要求多个线程使用的是同一个对象
public class MyTicketsTest2 {
public static void main(String[] args) {
MyTicket2 mt = new MyTicket2();
new Thread(mt,"北京窗口").start();
new Thread(mt,"西双版纳窗口").start();
new Thread(mt,"布宜诺斯艾利斯窗口").start();
}
}
class MyTicket2 implements Runnable {
int tickets = 100;
Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized(obj) {
if (tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "还剩" + tickets-- + "张票");
} else {
break;
}
}
}
}
}
上述代码依然存在线程安全问题 , 因为锁对象的选取不符合条件
解决方案 :
-
-
通过构造方法传入同一个obj
public class MyTicketsTest3 { public static void main(String[] args) { Object obj = new Object(); MyTicket3 mt1 = new MyTicket3(obj); mt1.setName("迪丽热巴"); MyTicket3 mt2 = new MyTicket3(obj); mt2.setName("古力娜扎"); MyTicket3 mt3 = new MyTicket3(obj); mt3.setName("马尔扎哈"); mt1.start(); mt2.start(); mt3.start(); } } class MyTicket3 extends Thread { static int tickets = 100; private Object obj; public MyTicket3(Object obj) { this.obj = obj; } @Override public void run() { while (true) { synchronized (obj) { if (tickets > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "还剩" + tickets-- + "张票"); } else { break; } } } } }
-
-
- 使用静态的对象
-
- 使用字符串的常量
-
- 使用class对象 : 在JVM中 , 每一个类的class对象总共只有一个
同步方法
- 把synchronized放到方法的修饰符中 , 锁的是整个方法
- 默认的锁对象
- 成员方法 : this
- 静态方法 : 类名.class
public class MyTicketsTest3 {
public static void main(String[] args) {
Object obj = new Object();
MyTicket3 mt1 = new MyTicket3(obj);
mt1.setName("迪丽热巴");
MyTicket3 mt2 = new MyTicket3(obj);
mt2.setName("古力娜扎");
MyTicket3 mt3 = new MyTicket3(obj);
mt3.setName("马尔扎哈");
mt1.start();
mt2.start();
mt3.start();
}
}
class MyTicket3 extends Thread {
static int tickets = 100;
private Object obj;
public MyTicket3(Object obj) {
this.obj = obj;
}
@Override
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "还剩" + tickets-- + "张票");
} else {
break;
}
}
}
}
}
上述代码虽然解决了线程安全问题 , 但是变成了单线程的程序 , 原因是锁的范围太大了导致的 ; 在使用同步方法的时候也要注意这个问题
- 单例设计 : 解决懒汉式的线程安全问题
public class SingInstanceDemo1 {
public static void main(String[] args) {
MyInstance ms1 = new MyInstance();
MyInstance ms2 = new MyInstance();
MyInstance ms3 = new MyInstance();
ms1.start();
ms2.start();
ms3.start();
}
}
class SingleInstance {
private static SingleInstance single = null;
private SingleInstance() {};
public synchronized static SingleInstance getInstance(){
if (single == null) {
single = new SingleInstance();
}
return single;
}
}
class MyInstance extends Thread{
@Override
public void run() {
SingleInstance s1 = SingleInstance.getInstance();
System.out.println(s1);
}
}
Lock
JDK1.5产生的新特性 : ReentrantLock
- lock : 释放锁的代码要放到finally代码块中 , 让他一定会执行到 , 否则可能会造成程序阻塞
- unlock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyTicketsTest4 {
public static void main(String[] args) {
MyTickets4 mt4 = new MyTickets4();
new Thread(mt4,"大白").start();
new Thread(mt4,"小黑").start();
new Thread(mt4,"黑白").start();
}
}
class MyTickets4 implements Runnable{
int tickets = 100;
Lock lock = new ReentrantLock();;//也要保证该锁对象对于多个线程是同一个
@Override
public void run() {
while (true) {
lock.lock();//加锁
try {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "还剩" + tickets-- + "张票");
} else {
break;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();//释放锁
}
}
}
}
死锁
- 死锁 : 是指两个或者两个以上的线程在执行的过程中 , 因争夺资源产生的一种互相等待的现象
public class DiedLock {
public static void main(String[] args) {
new MyThread(true).start();
new MyThread(false).start();
}
}
class Lock {
public static Object lockA = new Object();
public static Object lockB = new Object();
}
class MyThread extends Thread {
private boolean flag;
public MyThread(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
while (true) {
if (flag) {
synchronized (Lock.lockA) {
System.out.println("lockA");
synchronized (Lock.lockB) {
System.out.println("lockB");
}
}
} else {
synchronized (Lock.lockB) {
System.out.println("lockB");
synchronized (Lock.lockA) {
System.out.println("lockA");
}
}
}
}
}
}
线程池
- 为什么要是使用线程池 ?
- 减少了创建和销毁线程的次数 , 每个工作线程都可以被重复利用 , 可执行多个任务
- 可以根据系统的承受能力 , 调整线程池中工作线线程的数目 , 防止因为消耗过多的内存 , 而把服务器累趴下 ( 每个线程需要大约1MB内存 , 线程开的越多 , 消耗的内存也就越大 , 随后死机 )
pubic static ExecutorService newCachedThreadPool() //创建一个具有缓存功能的线程池(60秒)(21亿)
public static ExecutorService newFixedThreadPool(int nThreads) //创建一个可重用的,具有固定线程数的线程池
public static ExecutorService newSingleThreadExecutor()
//创建一个只有单线程的线程池,相当于上个方法的参数是1
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo1 {
public static void main(String[] args) {
/*ExecutorService pool = Executors.newCachedThreadPool();
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println("hhhhhhhh");
}
});*/
ExecutorService ntp = Executors.newFixedThreadPool(3);
ntp.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("haha");
}
});
ntp.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("hehe");
}
});
ntp.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("heihei");
}
});
ntp.execute(new Runnable() {
@Override
public void run() {
System.out.println("hiahia");
}
});
}
}
wait/notify/notifyAll
-
得到锁和释放锁
- 当一个线程进入了同步代码块 , 并开始执行了里面的代码 , 就叫做获得锁
- 当他执行完毕后 , 退出代码块 , 允许其他程序进入同步代码块就叫做释放锁
-
wait , notify , notifyAll 这三个方法都是Object中的方法 , 并且这三个方法必须在同步方法或同步代码块中使用 ( 锁对象 )
- wait : 让线程进入等待状态 , 进入等待状态的线程会释放锁对象 ( 也就是允许其他线程进入同步代码块 ) , 直到使用相同的锁对象调用notify/notifyAll方法才会结束阻塞状态
- notify : 唤醒 , 会从等待池中唤醒一个处于等待状态的线程唤醒 , 使其进入可执行状态
- notifyAll : 唤醒全部 , 会将等待池中所有处于等待状态的线程唤醒 , 使其进入可执行状态
-
notify或notifyAll以后 , 被唤醒的线程并不是立马执行 , 需要等到notify , notifyAll所在的代码块执行完毕后才会执行 , 因为只有同步代码块执行完毕后 , 才会释放锁对象 , 其他线程才可以进来
-
wait方法会释放锁对象 , 也就是一个线程使用wait进入等待状态后 , 允许其他线程进入同步代码块
- sleep方法不会释放锁对象 , 到时间自己醒
-
wait , notify , notifyAll三个方法为什么位于Object类中 ?
- 因为这三个方法必须使用锁对象调用 , 锁对象可以是任意对象 , 锁在Object类中
public class WaitNotifyDemo1 {
public static void main(String[] args) {
Object obj = new Object();
new Thread(new WaitThread(obj)).start();
new Thread(new WaitThread(obj)).start();
new Thread(new WaitThread(obj)).start();
new Thread(new WaitThread(obj)).start();
}
}
class WaitThread extends Thread {
private Object obj;
int num = 20;
public WaitThread(Object obj) {
this.obj = obj;
}
@Override
public void run() {
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "wait...start...");
try {
obj.wait();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "wait...end...");
}
}
}
class NotifyThread extends Thread {
private Object obj;
public NotifyThread() {
this.obj = obj;
}
@Override
public void run() {
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "notify...start...");
obj.notifyAll();
System.out.println(Thread.currentThread().getName() + "notify...end...");
}
}
}
定时器
框架 : framework
public Timer() //得到Timer对象
public void schedule(TimerTask task, long delay) //延迟多少毫秒后执行定时任务
public void schedule(TimerTask task,Date date) //指定时间执行定时任务
public void schedule(TimerTask task,long delay,long period) //延迟执行,指定间隔后循环执行
public void cancel() //取消定时任务
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class SocketDemo1 {
public static void main(String[] args) throws Exception {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("该起床了");
}
}, 3000);
String s = "2020-7-18 9:26:40";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date d = sdf.parse(s);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("早上八点必须起床");
timer.cancel();
}
}, d);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("C4 5秒后循环爆炸");
}
}, 5000,2000);
}
}