- 进程:正在运行中的程序(直译)。
- 线程:进程中一个负责程序执行的控制单元(执行路径)。
- 多线程: 一个进程中可以多执行路径,称之为多线程。
- 多线程的优缺点:
- 1. Java中实现多线程的手段:
- 2. Thread类中的 start() 方法和 run() 方法的区别:
- 3. 多线程的状态 (重要)
- 4. 卖票(使用Runnable接口,将票封装在任务中,只有一个票对象)
- 5. 线程安全问题产生的原因:
- 6. 同步代码块可以解决以上问题
- 7. 同步代码块的好处和弊端:
- 8. 同步函数使用的锁是 this
- 9. 静态的同步函数使用的锁是 该函数所属字节码文件对象, 可以使用getClass()来获取, 也可以使用 当前类名.class
- 10. 单例中的懒汉式存在多线程安全隐患
- 11. 死锁
- 12. 等待唤醒机制
- 13. 等待唤醒中 多生产多消费问题 ☆☆☆☆
- 单生产单消费, 因为一共有两个线程, 所以唤醒时必定会唤醒对方的线程, 不会出现多生产多消费的问题
- 多生产多消费有什么问题?
- 1. 使用 if 来判断标记, 生产或者消费方唤醒的仍是本方的线程的话, 本方线程不会再去判断标记, 导致生产或者消费执行多次.
- 解决方式: 将 if 判断 改为 while 循环, 醒来之后仍能判断标记, 避免了多次生产或者消费
- 2. 将if改为while之后会出现的问题: 如果对方线程全部wait(), 只剩本的一个线程在运行, 执行完一次后唤醒的仍然是本方线程, 会导致所有线程全部被wait();
- 解决方式: 在唤醒时, 至少能够保证唤醒对方一个线程, 才不会出现该结果, 但无法指定唤醒, 所以只能将 notify() 改为 notifyAll();
- 3. JDK1.5 之后将同步和锁封装成了对象, 来解决多生产和多消费的问题。
- 14. wait 和 sleep 的区别:
- 15. 停止线程的方法
进程:正在运行中的程序(直译)。
线程:进程中一个负责程序执行的控制单元(执行路径)。
多线程: 一个进程中可以多执行路径,称之为多线程。
多线程的优缺点:
多线程的好处:解决了多部分同时运行的问题。
多线程的弊端:线程太多会导致效率的降低。
1. Java中实现多线程的手段:
1)、继承Thread类。
// 定义一个类,继承Thread,重写run方法
class ThreadSub extends Thread {
private String name; // 为了区分线程
public ThreadSub(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(name + "执行" + i);
}
}
}
// main函数
public static void main(String[] args) {
Demo1Sub sub1 = new Demo1Sub("多线程01号");
Demo1Sub sub2 = new Demo1Sub("多线程02号");
sub1.start();
sub2.start();
}
打印结果:
多线程01号执行0
多线程02号执行0
多线程02号执行1
多线程02号执行2
多线程01号执行1
多线程01号执行2
2)、实现Runnable接口。
实现Runnable接口的好处:
1,将线程的任务从线程的子类中分离出来,进行了单独的封装。
按照面向对象的思想将任务的封装成对象。
2,避免了java单继承的局限性。
// Runnable接口详情, 只有一个抽象方法
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
// 定义一个类,实现Runnable接口,实现未实现的run方法
class ThreadSub2 implements Runnable {
private String name; // 为了区分线程
public ThreadSub2(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(name + "执行" + i);
}
}
}
// main函数
public static void main(String[] args) {
Demo1Sub2 sub1 = new Demo1Sub2("多线程01号");
Demo1Sub2 sub2 = new Demo1Sub2("多线程02号");
Thread t1 = new Thread(sub1);
Thread t2 = new Thread(sub2);
t1.start();
t2.start();
}
打印结果:
多线程01号执行0
多线程02号执行0
多线程02号执行1
多线程02号执行2
多线程01号执行1
多线程01号执行2
3)、匿名函数(其实就两种 这还是实现Runnable的一种)
// 定义一个类
class ThreadSub {
private String name; // 为了区分线程
public ThreadSub(String name) {
this.name = name;
}
// 该类中的方法
public void show() {
for (int i = 0; i < 3; i++) {
System.out.println(name + "执行" + i);
}
}
}
// 主函数入口
public static void main(String[] args) {
Demo1Sub sub1 = new Demo1Sub("多线程01号");
Demo1Sub sub2 = new Demo1Sub("多线程02号");
// 使用匿名函数直接调用 start 方法
new Thread(new Runnable() {
@Override
public void run() {
sub1.show();
}
}).start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
sub2.show();
}
});
t2.start()
}
打印结果:
多线程01号执行0
多线程02号执行0
多线程02号执行1
多线程02号执行2
多线程01号执行1
多线程01号执行2
2. Thread类中的 start() 方法和 run() 方法的区别:
run()方法: 在本线程内调用该Runnable对象的run()方法,可以重复多次调用;
start()方法: 启动一个线程,调用该Runnable对象的run()方法,不能多次启动一个线程
直接调用run()方法,相当于没有使用多线程技术,需要在run()方法体执行完毕后才会执行下面的代码,还是只有一个主线程的执行路径,按照顺序执行代码。
调用start()方法后,会来启动一个线程,这时线程处于就绪状态,真正实现了多线程。
3. 多线程的状态 (重要)
4. 卖票(使用Runnable接口,将票封装在任务中,只有一个票对象)
package se.thread;
/*
* 需求: 卖票
*
* 一共有100张票,使用四个线程同时卖票。
* */
public class SaleTicket {
public static void main(String[] args) {
// 使用继承Thread 类
// 既然创建多个线程会有多个车票对象,是否可以创建一个线程,开启四次?
// 答案是否定,会在main函数中抛出异常,非法的线程状态异常IllegalThreadStateException
// 一个线程只能被开启一次
/*
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
Ticket t4 = new Ticket();
t1.start();
t2.start();
t3.start();
t4.start();
*/
// 使用实现Runnable 接口
Ticket t = new Ticket(); // 创建一个线程任务。
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
// 继承Thread后创建了四个线程,导致每个线程对象都有一个自己的num,就会导致有400张票。
// 实现Runnable创建一个线程任务对象,该对象中有num属性,而其他线程将此对象作为参数,都会使用此对象的num属性
class Ticket /*extends Thread*/ implements Runnable {
// 继承时使用static可以解决此问题,但是很多情况我们是不想数据共享的。
// private static int num = 100;
private int num = 100;
/*@Override
public void run() {
while (true) {
if(num > 0) {
// 注意!!!!!这里存在安全隐患
// 即A线程通过了num>0的判断后,CPU不再执行A,执行B也通过了num>0的判断
System.out.println(Thread.currentThread().getName()
+ "......sale....." + (--this.num));
}
}
}*/
@Override
public void run() {
while (true) {
// 同步 来解决安全隐患
synchronized(Ticket.class){
if(num > 0) {
// sleep 用来演示临时阻塞状态下的安全隐患
// Thread.sleep(time),存在InterruptedException异常
// 由于实现的Runable接口,并且该接口没有声明过InterruptedException异常,
// 所以run方法中的异常只能catch
try { Thread.sleep(20); } catch (InterruptedException e) { }
System.out.println(Thread.currentThread().getName()
+ "......sale....." + (--this.num));
}
}
}
}
}
5. 线程安全问题产生的原因:
(1)多个线程在操作共享的数据。
(2)操作共享数据的线程代码有多条。(例如售票的,run方法中需要判断 num是否大于零再进行售票,就会有安全隐患)
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算。就会导致线程安全问题的产生。
6. 同步代码块可以解决以上问题
解决思路;
就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程时不可以参与运算的。
必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
7. 同步代码块的好处和弊端:
同步的好处:解决了线程的安全问题。
同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁。
同步的前提:同步中必须有多个线程并使用同一个锁。
8. 同步函数使用的锁是 this
9. 静态的同步函数使用的锁是 该函数所属字节码文件对象, 可以使用getClass()来获取, 也可以使用 当前类名.class
10. 单例中的懒汉式存在多线程安全隐患
11. 死锁
容易出现死锁的一种形式: 同步嵌套(着重看打印结果以及解释)
package com.lius.javase.demo.thread;
/**
* 死锁:
* 容易出现死锁的一种形式: 同步嵌套
*/
public class DeathLock implements Runnable{
public boolean flag;
DeathLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag) {
synchronized (MyLock.LOCK_A) {
System.out.println(Thread.currentThread().getName() + "...if...LOCK_A");
synchronized (MyLock.LOCK_B) {
System.out.println(Thread.currentThread().getName() + "...if...LOCK_B");
}
}
} else {
synchronized (MyLock.LOCK_B) {
System.out.println(Thread.currentThread().getName() + "...else...LOCK_B");
synchronized (MyLock.LOCK_A) {
System.out.println(Thread.currentThread().getName() + "...else...LOCK_A");
}
}
}
}
}
class MyLock {
public static final Object LOCK_A = new Object();
public static final Object LOCK_B = new Object();
}
class Test {
public static void main(String[] args) {
DeathLock target1 = new DeathLock(true);
DeathLock target2 = new DeathLock(false);
Thread t1 = new Thread(target1);
Thread t2 = new Thread(target2);
t1.start();
t2.start();
}
}
死锁的打印结果:
Thread-1...else...LOCK_B
Thread-0...if...LOCK_A
对打印结果进行解释:
线程1的标志位(flag)为false, 走了else分支, 拿着B锁进入了else里的第一个同步代码块,
在想要拿A锁进入else的下一个代码块的时候, 线程1失去了CPU的执行权, 进入到阻塞状态, 此时线程2抢到执行权;
线程2的标志位(flag)是true, 走了if分支, 拿着A锁进入了if里的第一个同步代码块,
在想要拿B锁进入if的下一哥代码块的时候, B锁却还在线程1手里, 所以拿不到, 就发生了死锁现象.
12. 等待唤醒机制
代码示例(同一个资源, 一个线程负责存值, 另一个线程负责取值)
package com.lius.javase.demo.thread;
/**
* 同一个资源, 一个赋值, 一个取值
*
* 使用等待唤醒机制:
* 设计到的方法:
* wait(): 让线程处于冻结状态, 被wait()的线程会被存放到线程池中.
* notify(): 唤醒线程池中的一个线程(随机的).
* notifyAll(): 唤醒线程池中的所有线程.
*
* 这些方法都必须定义在同步中,
* 因为这些方法都是改变线程状态的方法, 必须要明确操作的到底是哪个锁上的线程.
*/
public class ResourceDemo {
public static void main(String[] args) {
Resource r = new Resource();
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
}
}
// 资源
class Resource {
private String name;
private String sex;
private boolean isExists = false; // 是否已有值
public synchronized void set(String name, String sex) {
if(isExists) {
try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
} /* 注意!!! 这里不能使用 else 代码块 */
this.name = name;
this.sex = sex;
isExists = true;
this.notify();
}
public synchronized void print() {
if(!isExists) {
try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
} else {
System.out.println(this.name + "..." + this.sex);
isExists = false;
this.notify();
}
}
}
// 赋值
class Input implements Runnable{
private final Resource r;
Input(Resource r) {
this.r = r;
}
boolean flag = false; // 控制赋值切换的
@Override
public void run() {
for(;;) {
if (flag) {
r.set("mike", "man");
} else {
r.set("丽丽", "女女女女女女女女女");
}
flag = !flag;
}
}
}
// 取值
class Output implements Runnable {
private final Resource r;
Output(Resource r) {
this.r = r;
}
@Override
public void run() {
for(;;) {
r.print();
}
}
}
为什么操作线程的方法wait, notify, notifyAll 定义在了Object类中?
因为这些方法是监视器的方法。监视器其实就是锁。锁可以是任意的对象,任意的对象调用的方式一定定义在Object类中。
13. 等待唤醒中 多生产多消费问题 ☆☆☆☆
单生产单消费, 因为一共有两个线程, 所以唤醒时必定会唤醒对方的线程, 不会出现多生产多消费的问题
多生产多消费有什么问题?
1. 使用 if 来判断标记, 生产或者消费方唤醒的仍是本方的线程的话, 本方线程不会再去判断标记, 导致生产或者消费执行多次.
解决方式: 将 if 判断 改为 while 循环, 醒来之后仍能判断标记, 避免了多次生产或者消费
2. 将if改为while之后会出现的问题: 如果对方线程全部wait(), 只剩本的一个线程在运行, 执行完一次后唤醒的仍然是本方线程, 会导致所有线程全部被wait();
解决方式: 在唤醒时, 至少能够保证唤醒对方一个线程, 才不会出现该结果, 但无法指定唤醒, 所以只能将 notify() 改为 notifyAll();
package com.lius.javase.demo.thread;
/**
* 多生产者 -- 多消费者
*
* 多生产多消费出现的问题:
* 1. 使用 if 判断标记, 被 notify 后不会再判断标记了, 直接执行后面的代码, 需要重新判断标记, 将 if 改为 while
* 2. 将 if 改为 while 后, 造成所有线程全部被 wait(), 即死锁的另外一种情况, 如何解决?
* 因为至少需要唤醒一个对方的线程, 但是没有指定唤醒, 所以干脆全唤醒, 使用 notifyAll()
*/
public class ProducerConsumerDemo {
public static void main(String[] args) {
Resource r = new Resource();
Producer producer = new Producer(r);
Consumer consumer = new Consumer(r);
Thread t0 = new Thread(producer);
Thread t1 = new Thread(producer);
Thread t2 = new Thread(consumer);
Thread t3 = new Thread(consumer);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
class Resource {
private String name;
private int index = 1;
private boolean flag = false;
public synchronized void set(String name) {
while (this.flag) { // 这里将 if 改为 while 是为了醒来之后再去判断标记, 以免本方线程多次执行
try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}
}
this.name = name + index;
index++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
this.flag = true;
// notify() 改为 notifyAll()
// 因为使用了 while 会导致所有线程全部被 wait(),
// 所以必须保证唤醒时可以至少唤醒对面一个, 但无法指定唤醒那个线程, 干脆全部唤醒
this.notifyAll();
}
public synchronized void out() {
while (!this.flag) { // 这里改为 while 与上同理
try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}
}
System.out.println(Thread.currentThread().getName() + "......消费者......" + this.name);
this.flag = false;
this.notifyAll(); // notify() 改为 notifyAll() 与上同理
}
}
class Producer implements Runnable {
private final Resource r;
Producer(Resource r) {
this.r = r;
}
@Override
public void run() {
for(;;) {
r.set("烤鸭");
}
}
}
class Consumer implements Runnable {
private final Resource r;
Consumer(Resource r) {
this.r = r;
}
@Override
public void run() {
for(;;) {
r.out();
}
}
}
3. JDK1.5 之后将同步和锁封装成了对象, 来解决多生产和多消费的问题。
package com.lius.javase.demo.thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 多生产多消费, 使用Lock接口替换同步代码块或者同步函数 (JDK1.5之后才有的)
*/
public class ProConLockDemo {
public static void main(String[] args) {
Resource r = new Resource();
Pro producer = new Pro(r);
Cons consumer = new Cons(r);
Thread t0 = new Thread(producer);
Thread t1 = new Thread(producer);
Thread t2 = new Thread(consumer);
Thread t3 = new Thread(consumer);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
class Resource {
private String name;
private int index = 1;
private boolean flag = false;
Lock lock = new ReentrantLock();
// 通过一个锁上挂多组监视器, 来解决 notifyAll() 的效率问题
Condition con_condition = lock.newCondition(); // 生产者监视器
Condition pro_condition = lock.newCondition(); // 消费者监视器
public void set(String name) {
lock.lock();
try {
while (this.flag) {
try {
con_condition.await();} catch (InterruptedException e) {e.printStackTrace();}
}
this.name = name + index;
index++;
System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
this.flag = true;
pro_condition.signal();
} finally {
lock.unlock();
}
}
public void out() {
lock.lock();
try {
while (!this.flag) {
try {
pro_condition.await();} catch (InterruptedException e) {e.printStackTrace();}
}
System.out.println(Thread.currentThread().getName() + "......消费者......" + this.name);
this.flag = false;
con_condition.signal();
} finally {
lock.unlock();
}
}
}
class Pro implements Runnable {
private final Resource r;
Pro(Resource r) {
this.r = r;
}
@Override
public void run() {
for(;;) {
r.set("烤鸭");
}
}
}
class Cons implements Runnable {
private final Resource r;
Cons(Resource r) {
this.r = r;
}
@Override
public void run() {
for(;;) {
r.out();
}
}
}