day18
1.多线程概述
-
进程:一个程序运行,程序在内存中分配的那片空间。
-
线程:进程中一个执行单元执行路径
进程中至少有一个线程,如果进程中有多个线程,就是多线程的程序。
-
并行与并发:
并行:某一时间点,有多个程序同时执行,多核CPU运行 并发:某一时间段,有多个程序同时执行,并不是真正意义的同时执行。 为多线程。
-
并发真的是同时执行吗?
不是,而是时间间隔很短,造成同时执行感觉。
-
多线程优势?
提高了用户体验,提高了程序的运行效率,提高CPU使用率。
2.开启线程两种方式
-
开启线程
/* * 1.继承Thread * 2.重写run方法 * 3.创建子类的对象 * 4.调用start方法 * */ public class ThreadDemo1 { public static void main(String[] args) { // 创建子类的对象 Demo d1 = new Demo("Jack"); Demo d2 = new Demo("Tom"); // 设置线程名字 d1.setName("d1"); // 获取d1执行线程名字 System.out.println(d1.getName()); // 获取当前线程名字 System.out.println(Thread.currentThread().getName()); // 调用start方法 d1.start(); d2.start(); } } // 继承Thread class Demo extends Thread{ String nickName; public Demo(String nickName) { this.nickName = nickName; } // 重写run方法 public void run() { for(int i=0;i<30;i++) { System.out.println(nickName + "---" + i); } } } // 整个运行过程有三个线程运行,主线程开启d1和d2线程
-
run方法与start方法区别
start:开启新的线程,会自动调用run方法在新的线程中执行 run:没有开启新的线程,只是普通方法
-
开启新线程第二种方式
声明实现Runnable接口的类,该类然后实现run方法,然后可以分配该类的实例,在创建Thread时做为一个参数来传递并启动,采用这种风格的同一个例子
/* * 实现多线程第二种方式: * 1.实现Runnable * 2.重写run方法 * 3.创建Runnable子类的对象 * 4.创建Thread类的对象,把第三步的对象传到构造方法中 * 5.使用Thread子类对象,调用start方法 * */ public class ThreadDemo2 { public static void main(String[] args) { Demo5 d = new Demo5();// 只有Thread或子类线程对象才是线程对象。它只是线程任务度夏宁。 Thread th = new Thread(d); // th才是线程对象 Thread th2 = new Thread(d); th.start(); th2.start(); } } class Demo5 implements Runnable{ public void run() { for (int i=0;i<20;i++) { System.out.println(Thread.currentThread().getName() + "---" + i); } } }
-
两种实现方式,区别是?
第一种方式有局限性的,因为Java是单继承,如果一个类已经有一个继承,它就不能再继承Thread类,就无法实现多线程。而第二种实现通过接口方式实现更合理,并且第二种方式更加符合面向对象特点:高内聚低耦合,把线程对象和线程任务分离开了。
3.线程中方法
1.sleep方法使用
public Static void sleep(long millis)
静态方法 进入阻塞状态,时间结束后进入可执行
- 示例:
Thread.sleep(3000);
sleep方法让谁阻塞,取决于他在哪个线程中。
2.join方法使用
public final void join()
被谁调用,让哪个线程先执行,执行完毕后,再执行所在线程
- 示例
public class JoinDemo1 {
public static void main(String[] args) {
Sum s = new Sum();
s.start();
// join:用谁调用,就让那个线程先执行,执行完毕后,再执行他所在线程(将他所在线程阻塞)。
try {
s.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Sum.sum);
}
}
class Sum extends Thread{
static int sum = 0;
public void run() {
for(int i=0;i<=1000;i++) {
sum += i;
}
}
}
3.yield方法
public static void yield()
让其他线程先执行,不一定生效,因为让谁执行是CPU决定的
4.stop方法
停止一个线程
5.interrupt 方法
打断线程的阻塞状态,进入可执行状态,会抛出异常
- 示例:打断子线程阻塞
public class interruptDemo {
public static void main(String[] args) {
Demo3 d = new Demo3();
d.start();
// 将d执行线程阻塞状态打断。
d.interrupt();
System.out.println("over");
}
}
class Demo3 extends Thread{
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
for (int i=0;i<10;i++) {
System.out.println(i);
}
}
}
- 打断主线程阻塞
public class InterruptDemo2 {
public static void main(String[] args) {
// 主线程传给Demo4
Demo4 d = new Demo4(Thread.currentThread());
d.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("over");
}
}
class Demo4 extends Thread{
Thread th;
public Demo4(Thread th) {
this.th = th;
}
// 用于打断主线程
public void run() {
th.interrupt();
}
}
- 练习:创建两个线程,一个线程负责打印大写字母表,一个线程负责打印小写字母表
public class Threadlianxi1 {
public static void main(String[] args) {
Thread th1 = new Thread(new Task1());
Thread th2 = new Thread(new Task2());
th1.start();
th2.start();
}
}
class Task1 implements Runnable{
public void run() {
for(char i='a';i<='z';i++) {
System.out.println(i);
}
}
}
class Task2 implements Runnable{
public void run() {
for(char i='A';i<='Z';i++) {
System.out.println(i);
}
}
}
4.线程生命的周期
主线程执行时候在栈空间,开辟空间给子线程,而他们的之间栈是独立的,但他们堆空间数据是共享的
5.线程安全的问题
- 买票示例:
public class SellTicketsDemo {
public static void main(String[] args) {
Tickets t = new Tickets();
Thread th1 = new Thread(t);
Thread th2 = new Thread(t);
Thread th3 = new Thread(t);
th1.start();
th2.start();
th3.start();
}
}
class Tickets implements Runnable{
static int tickets = 100;
public void run() {
while (true) {
if (tickets> 0) {
try {
Thread.sleep(30);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "====" + "正在出售第" + tickets-- +"张票!");
} else {
break;
}
}
}
}
-
在执行代码时候,会发现多个线程会卖出同一张票。这样会产生线程安全问题。
-
线程安全产生原因:1.具备多线程。2.操作共享数据。3.操作共享数据的代码有多条。
-
通过加锁:让每一时刻只能有一个线程操作数据。方式有三种
1.同步代码块
synchronized(锁对象){
容易产生线程安全问题的代码
}
// 锁对象:可以是任意对象,但是必须保证多个线程使用是同一个对象。
- 示例:
- 关键点:锁对象选择
public class SellTicketsDemo {
public static void main(String[] args) {
Tickets t = new Tickets();
Thread th1 = new Thread(t);
Thread th2 = new Thread(t);
Thread th3 = new Thread(t);
th1.start();
th2.start();
th3.start();
}
}
class Tickets implements Runnable{
static int tickets = 100;
// 如果o方法run方法里,则无法实现线程安全,原因是执行run方法,实现3个o对象,对于这三个线程来说o对象不是共有的同一个对象。
Object o = new Object();
public void run() {
while (true) {
synchronized (o) {// o所在的类被new几次
if (tickets> 0) {
try {
Thread.sleep(30);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "====" + "正在出售第" + tickets-- +"张票!");
} else {
break;
}
}
}
}
}
- 锁对象选取错误示例:
- Tickets2被new了3次导致,没有锁住。
public class SellTicketsDemo2 {
public static void main(String[] args) {
Tickets2 th1 = new Tickets2();
Tickets2 th2 = new Tickets2();
Tickets2 th3 = new Tickets2();
th1.start();
th2.start();
th3.start();
}
}
class Tickets2 extends Thread{
static int tickets = 100;
Object o = new Object();
public void run() {
while (true) {
synchronized (o) {
if (tickets> 0) {
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "====" + "正在出售第" + tickets-- +"张票!");
} else {
break;
}
}
}
}
}
- 方式1:解决方式通过:构造方法解决:
public class SellTicketsDemo2 {
public static void main(String[] args) {
Object o = new Object();
Tickets2 th1 = new Tickets2(o);
Tickets2 th2 = new Tickets2(o);
Tickets2 th3 = new Tickets2(o);
th1.start();
th2.start();
th3.start();
}
}
class Tickets2 extends Thread{
static int tickets = 100;
Object o;
// 构造方法
public Tickets2(Object o) {
this.o=o;
}
public void run() {
while (true) {
synchronized (o) {
if (tickets> 0) {
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "====" + "正在出售第" + tickets-- +"张票!");
} else {
break;
}
}
}
}
}
- 方式2:使用静态方式:
Static Object o = new Object();
- 方式3:使用字符串的常量
synchronized ("abc")
- 方式4:使用Class对象
synchronized (SellTicketsDemo2.class)
// 在jvm中,每一个类的Class总共只有一个
2.同步方法
- 把synchronized放到方法的修饰符中,锁的是整个方法。
public class SellTicketsDemo3 {
public static void main(String[] args) {
Tickets3 th1 = new Tickets3();
new Thread(th1).start();
new Thread(th1).start();
new Thread(th1).start();
}
}
class Tickets3 implements Runnable{
static int tickets = 100;
// 默认锁对象:this
public synchronized void run() {
while (true) {
if (tickets> 0) {
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "====" + "正在出售第" + tickets-- +"张票!");
} else {
break;
}
}
}
}
上述代码虽然解决了线程安全问题,但是编程了单线程程序,原因synchronized锁的范围太大,第一个进来线程,执行完整个while循环。导致在使用同步方法时候也需要注意这个问题。
- 但可以通过同步方法解决懒汉式单例模式:
package com.xjk;
// 懒汉式:存在问题,存在线程安全问题
public class Singleton2 {
private static Singleton2 s;
// 构造方法私有化,为了不让别人随便new
private Singleton2() {
}
// 通过synchronized 解决线程安全问题
public synchronized static Singleton2 getInstance() {
if (s == null) {
s = new Singleton2();
}
return s;
}
}
// 当启100个线程,当一个线程执行到s=new Singleton2();此时刚要new Singleton2,cpu切到第二个线程,因为s此时还是等于null,第二个线程也执行new Singleton2() 导出单例模式创建多个对象。此时通过同步方法添加synchronized可以有效解决此问题。
- 同样StringBuffer是线程安全的内部有synchronized,效率相对StringBuilder低,StringBuilder是线程不安全的。
3.Lock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SellTicketsDemo4 {
public static void main(String[] args) {
Tickets4 th1 = new Tickets4();
new Thread(th1).start();
new Thread(th1).start();
new Thread(th1).start();
}
}
class Tickets4 implements Runnable{
static int tickets = 100;
// 也要保证该锁对象对于多个线程是同一个
Lock lock = new ReentrantLock();
public synchronized void run() {
while (true) {
lock.lock();// 加锁
try {
if (tickets> 0) {
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "====" + "正在出售第" + tickets-- +"张票!");
} else {
break;
}
} finally {
lock.unlock();//解锁
}
}
}
}
- 释放锁的代码放到finally代码块中,否则容易造成程序阻塞。
6.死锁
- 是指两个或两个以上的线程在执行的过程中,因争夺资源产生一种互相等待现象。
7.线程池用法
-
线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
-
可以根据系统的承受能力,调整线程池中工作线程数目,放置因为消耗过多的内存,而把服务器累死(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
-
线程池的创建:
public static ExecutorService newCachedThreadPool() 创建一个具有缓存功能的线程池 public static ExecutorService new FixedThreadPool(int nThreads) 创建一个可重用的,具有固定线程数的线程池 public static ExecutorService newSingleThreadExecutor() 创建一个只有单线程的线程池,相当于上个方法的参数是1
-
示例:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolDemo { public static void main(String[] args) { // 1 ExecutorService pool = Executors.newCachedThreadPool(); pool.execute(new Runnable() { public void run() { System.out.println("hello world"); } }); // 关闭线程池 pool.shutdown(); // 2 pool = Executors.newFixedThreadPool(20); } } // 缓存线程池,如果没有任务会等待60s就关闭
8.wait/Notify/NotifyAll
- wait,notify,notifyAll这三个方法都是Object中方法,并且这三个方法必须在同步方法或同步代码块中使用。
wait: 让线程进入等待状态,进入等待状态线程会释放锁对象(也就是允许其他线程进入同步代码块),直到使用相同的锁对象调用notify/notifyAll方法才会结束阻塞状态。
notify:唤醒,会从等待池中唤醒一个处于等待状态的线程,使其进入可执行状态。
notifyAll:唤醒全部,会将等待池中所有处于等待状态的线程唤醒,使其进入可执行状态。
notify或notifyAll以后,被唤醒的线程并不是立马执行,需要等到notify,notifyAll所在代码块执行完毕后才会执行。因为只有同步代码块执行完毕后,才会释放锁对象,其他线程才可以进来。
wait方法会释放锁对象,也就是一个线程使用wait进入等待状态后,允许其他线程进入同步代码块。而sleep方法不会释放锁对象,到时间后自己会醒来。
- 示例:
public class WaitNotifyDemo {
public static void main(String[] args) {
Object o = new Object();
// 当使用
new Thread(new Demos1(o)).start();
new Thread(new Demos1(o)).start();
try {
// sleep 作用是保证上面2个线程都执行到wait,然后第三个线程可以使用notifyAll解除阻塞
Thread.sleep(30);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
new Thread(new Demos2(o)).start();
}
}
class Demos1 implements Runnable{
Object o;
public Demos1(Object o) {
this.o = o;
}
@Override
public void run() {
synchronized(o) {
System.out.println(Thread.currentThread().getName() + "Wait ,,,start...");
try {
// 阻塞线程
o.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("等待结束...");
}
}
}
class Demos2 implements Runnable{
Object o;
public Demos2(Object o) {
this.o = o;
}
@Override
public void run() {
synchronized(o) {
System.out.println(Thread.currentThread().getName() + "notify ,,,start...");
// 解除阻塞
// o.notify();
o.notifyAll();
System.out.println(Thread.currentThread().getName() + "notify ,,,end...");
}
}
}
/*
Thread-0Wait ,,,start...
Thread-1Wait ,,,start...
Thread-2notify ,,,start...
Thread-2notify ,,,end...
等待结束...
等待结束...
* */
注意:
1.wait.notify.notifyAll 一定要在同步代码块中执行,使用锁对象调用。
2.要想能唤醒wait,必须使用同一个锁对象调用notify/notifyAll
3.wait方法会释放锁对象,进入了等待状态以后,允许其他线程进入同步代码块执行。
4.notify方法唤醒了以后,wait不是立马执行,等待notify中代码执行完毕。
5.而notify方法只能唤醒一个(随机唤醒),而notifyAll全部都能唤醒
-
为什么wait,notify,notfiyAll 放到Object类中?
因为他们使用锁对象调用,锁对象可以是任意对象,任意对象都有的方法定义在Object
9.定时器
-
方法
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() 取消定时任务
-
示例1:
import java.util.Timer; import java.util.TimerTask; public class TimerDemo { public static void main(String[] args) { Timer timer = new Timer(); // 5000毫秒执行一次任务,同一个定时任务只能执行一次 // 它是多线程启动的 timer.schedule(new TimerTask() { public void run() { System.out.println("你好"); // 取消定时任务,一般放到定时任务中 timer.cancel(); } }, 5000); } }
-
示例2:日期定时
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class TimerDemo { public static void main(String[] args) throws ParseException { Timer timer = new Timer(); // 如果时间已经过期,它会立刻运行 Date d = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2020-10-10 10:10:10"); timer.schedule(new TimerTask() { public void run() { System.out.println("起床了!"); } }, d); } }
-
示例3:
import java.text.SimpleDateFormat; import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class TimerDemo2 { public static void main(String[] args) { // 3秒后执行,每隔1秒执行一次 new Timer().schedule(new TimerTask() { public void run() { System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); } },3000,1000); } }