- 程序(program):是为了完成特定任务、用某种语言编写的一组指令集合。即一段静态的代码,静态对象。
- 进程(process):是程序的一次执行过程,或是正在运行的一个程序。如:运行中的 qq 它就是一个进程。
- 线程(Thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。若一个程序可以在同一时间执行多个线程,就是支持多线程的。
何时需要多线程:
- 程序需要同时执行两个或多个任务
- 程序需要实现一些需要等待的任务时,如:用户输入,文件读写
- 需要一些后台运行的程序(如 java 的垃圾回收机制以守护线程的方式运行 )
多线程的创建和启动:
- Java 语言的 JVM 允许程序运行多个线程,它通过
java.lang.Thread
来实现- Thread 类的特性
- 每个线程都是通过某个特定 Thread 对象的 run() 方法来完成操作的,通常把 run() 方法的主体称为线程体。
- 通过该对象的 start() 方法来调用这个线程
/**
* 创建一个子线程,完成 1-100 之间自然数的输出,主线程执行同样的操作
*/
// 1.创建一个继承 Thread 类的子类
class SubThread extends Thread {
// 2.重写 Thread 类的 run() 方法,方法内实现此子线程要完成的功能
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("子线程---" + Thread.currentThread().getName() + i);
}
}
}
public class TestThread {
public static void main(String[] args) {
// 3.创建一个子类的对象
SubThread st = new SubThread();
// 4.调用线程 start 方法,①启动线程 ②调用相应的 run 方法
// 一个线程只能执行一次 start 方法
st.start();
for (int i = 0; i < 100; i++) {
System.out.println("主线程---" + Thread.currentThread().getName() + i);
}
}
}
Thread 类常用方法:
/** * Thread 的常用方法 * 1.start() 方法:启动线程,并执行相应的 run() 方法 * 2.run() 方法:子线程要执行的代码放入 run() 方法中 * 3.currentThread() 方法:静态方法,获取当前线程 * 4.getName() 方法:获取此线程的名字 * 5.setName() 方法:设置此线程的名字 * 6.yield() 方法:调用此方法的线程释放当前 cpu 的执行权 * 7.join() 方法:在 A 线程中调用 B 线程的 join() 方法,表示当执行到此方法,A 线程停止执行,直到 B 线程执行完毕,A 线程再接着 join() 之后的代码执行 * 8.isAlive() 方法:判断当前线程是否还存活 * 9.sleep(long l) 方法:显示的让当前线程睡眠 l 毫秒 * 10.线程通信:wait() 、 notify() 、notifyAll() * 11.设置线程的优先级: * getPriority():返回线程的优先值 * setPriority(int newPriority):设置线程的优先级(只能保证线程抢到 cpu 执行权的概率变大了) */
继承的方式实现火车站窗口售票:
package com.iflytek.thread;
/**
* 模拟火车站窗口售票,开启三个窗口售票,总票数为 100 张
*/
class Window extends Thread {
static int tickets = 100;
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "号窗口" + tickets--);
} else {
break;
}
}
}
}
public class TestWindow {
public static void main(String[] args) {
Window w1 = new Window();
w1.setName("1号");
w1.start();
Window w2 = new Window();
w2.setName("2号");
w2.start();
Window w3 = new Window();
w3.setName("3号");
w3.start();
}
}
实现的方式创建多线程:
/**
* 通过实现 Runnable 接口的方式创建多线程
*
*/
//1.创建一个实现 Runnable 接口的类
class PrintNum implements Runnable {
//2.重写 run 方法
@Override
public void run() {
// 子线程执行的代码
for(int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class TestThread1 {
public static void main(String[] args) {
// 3.创建一个实现 Runnable 接口类的对象
PrintNum p1 = new PrintNum();
//4.将此对象作为形参传递给 Thread 类的构造器中,创建 Thread 类的对象,此对象即为一个线程
Thread t1 = new Thread(p1);
// 要想启动一个线程,必须调用 start 方法
t1.start(); // 5.启动线程并执行 run 方法
Thread t2 = new Thread(p1);
t2.start();
}
}
实现的方式实现火车站售票:
/**
* 通过实现 Runnbale 接口的方式售票
*/
class Window1 implements Runnable {
int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "售的票号为:" + ticket--);
} else {
break;
}
}
}
}
public class TestWindow1 {
public static void main(String[] args) {
Window1 w1 = new Window1();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
t1.start();
t2.start();
}
}
JDK 中使用 Thread.State 枚举类表示线程的几种状态
- 要想实现多线程,必须在主线程中创建新的线程对象。 Java 语言使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下 5 中状态:
- 新建:
在 Java5.0 提供了 java.util.concurrent (简称 juc 包),在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现
在说 volatile 关键字与内存可见性之前,先看下面一段代码
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
Thread t = new Thread(td);
t.start();
while (true) {
if (td.isFlag()) {
System.out.println("-----------");
break;
}
}
}
}
class ThreadDemo implements Runnable {
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag = " + isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
这段代码有两个线程,一个主线程,一个子线程。子线程要做的事就是修改 flag 的值,主线程要做的事就是读取 flag 的值,如果 flag = true 就打印一连串虚线,并跳出循环。
运行结果:
flag = true
并且程序没有停止
共享变量在线程间的可见性:
- 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量
- 可见性:一个线程对共享变量值的修改,能够及时的被其它线程看到
说明:
- 所有变量都存储在主内存中
- 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
- 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从相互内存中读写
- 不同线程之间无法直接访问其他线程工作内存中的变量,线程之间变量值的传递需要通过主内存来完成
共享变量可见性实现原理:
把工作内存1 中更新过的共享变量刷新到主内存中,把主内存中最新的共享变量的值更新到工作内存2 中
volatile 关键字:当多个线程进行操作共享数据时,可以保证内存中的数据是可见的
class ThreadDemo implements Runnable {
// 在共享数据上加一个 volatile 关键字
private volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag = " + isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
/**
* i++ 的原子性问题: 实际上分为三个步骤:"读-改-写"
* int i = 10
* i = i++ // i = 10
*
* i = i++ 在计算机底层是这么操作的:
* int temp = i; 先把 i 赋给一个临时变量
* i = i + 1;
* i = temp; 再把临时变量赋值给 i
*/
public class TestAtomicDemo {
public static void main(String[] args) {
AtomicDemo ad = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(ad).start();
}
}
}
class AtomicDemo implements Runnable {
private int serialNumber = 0;
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getSerialNumber());
}
public int getSerialNumber() {
return serialNumber++;
}
}
// 上面这段代码运行的时候会出现线程安全问题(原子性问题)
// 0 1 0 4 2 0 3 7 5 6
下面分析一下上面线程安全产生的原因:
那么怎么解决上面产生的问题呢?
使用原子变量:
jdk1.5 以后 java.util.concurrent.atomic 包下提供了常用的原子变量
原子变量时如何解决上面的问题的:
1、原子变量中包含 volatile 关键字,保证内存可见性
2、CAS(Compare-And-Swap) 算法保证数据的原子性
- CAS 算法是硬件对于并发操作共享数据的支持
- CAS 算法包含了三个操作数
- 内存值 V
- 预估值 A
- 更新值 B
- 当且仅当 V == A 时,才把 V = B。否则,将不做任何操作
public class TestAtomicDemo {
public static void main(String[] args) {
AtomicDemo ad = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(ad).start();
}
}
}
class AtomicDemo implements Runnable {
// 和包装类使用差不多
private AtomicInteger serialNumber = new AtomicInteger();
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getSerialNumber());
}
public int getSerialNumber() {
// 自增
return serialNumber.getAndIncrement();
}
}
// 这样就不会有线程安全问题了
// 1 8 9 2 7 5 6 0 3 4
- CAS(Compare-And-Swap)是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问
- CAS 是一种无锁的非阻塞算法的实现
- CAS 包含了 3 个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
- 当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作
/**
* 模拟 cas 算法
*/
public class TestCompareAndSwap {
public static void main(String[] args) {
CompareAndSwap cas = new CompareAndSwap();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
int expectValue = cas.get();
boolean flag = cas.compareAndSet(expectValue, (int) Math.random());
System.out.println(flag);
}
}).start();
}
}
}
class CompareAndSwap {
// 内存值
private int value;
// 用于获取内存值
public synchronized int get() {
return value;
}
//比较
public synchronized int compareAndSwap(int expectValue, int newValue) {
// 在比较前都会先读一下内存值
int oldValue = value;
// 让内存值和预估值进行比较
if (oldValue == expectValue) {
// 如果一样,就替换
this.value = newValue;
}
return oldValue;
}
// 设置值
public synchronized boolean compareAndSet(int expectValue, int newValue) {
return expectValue == compareAndSwap(expectValue, newValue);
}
}
- java 5.0 在 java.util.concurrent 包中提供了多种并发容器类来改进同步容器的性能
- concurrentHashMap 同步容器类是 Java5 增加的一个线程安全的哈希表。对于多线程的操作,介于 HashMap 与 Hashtable 之间。内部采用 "锁分段" 机制替代 Hashtable 的独占锁,进而提高性能。
- 此包还提供了用于多线程上下文中的 Collection 实现:ConcurrentHashMap 、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。当期望许多线程访问一个给定 Collection 时,ConcurrentHashMap 通常优于同步的 HashMap。ConcurrentSkipListMap 通常优于同步的 TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList
/**
* 添加操作多时,效率低,因为每次添加时都会进行复制,开销非常大
* 但是当并发迭代操作时,可以选择,效率高
*/
public class TestCopyOnWriteArrayList {
public static void main(String[] args) {
HelloThread thread = new HelloThread();
for (int i = 0; i < 10; i++) {
new Thread(thread).start();
}
}
}
class HelloThread implements Runnable {
// synchronizedList 它的包装方式就是把 Arraylist 里面的方法都变成了同步方法
// private static List<String> list = Collections.synchronizedList(new ArrayList<String>());
private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
static {
list.add("AA");
list.add("BB");
list.add("CC");
}
@Override
public void run() {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
//边迭代边添加元素(会报 java.util.ConcurrentModificationException)
list.add("66");
// 使用 CopyOnWriteArrayList 每次添加时都会把列表复制一份
}
}
}
- CountDownLatch 一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
- 闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才继续执行。
- 确保某个计算在其需要的所有资源都被初始化之后才继续执行
- 确保某个服务在其依赖的所有其他服务都已启动之后才启动
- 等待直到某个操作所有参与者都准备就绪再继续执行
package com.iflytek.juc;
import java.util.concurrent.CountDownLatch;
/**
* CountDownLatch :闭锁,在完成某些运算时,只有其他所有线程的运算全部完成,当前运算才继续执行
*/
public class TestCountDownLatch {
public static void main(String[] args) {
// 这里初始化为 10,因为有 10 个子线程在执行,这里的 10 相当于一个计数器
final CountDownLatch latch = new CountDownLatch(10);
LatchDemo ld = new LatchDemo(latch);
/**
* 我现在开启 10 个子线程,想去算一下,30000 以内的偶数的执行时间
* 下面这种写法是有问题的,因为 10 个子线程,1 个主线程,这 11 个线程
* 是同时执行的,很可能都看不到最后打印的那句话
* 需要的做法应该是:等 10 个子线程执行完,然后我主线程再执行,算 10 个子线程的执行时间
*/
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
new Thread(ld).start();
}
try {
// countDown 在递减到 0 之前,主线程一直等待
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("执行时间:" + (end - start));
}
}
class LatchDemo implements Runnable {
private CountDownLatch latch;
public LatchDemo(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
// 为了防止有线程安全问题,加个锁
synchronized (this) {
try {
for (int i = 0; i < 30000; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
} finally {
// 一个线程执行完了之后,我需要把这个计数器递减 1
// 为了保证必须执行,所以放在 finally 中
latch.countDown();
}
}
}
}
除了实现 Runnable 接口和继承 Thread 接口,第三种创建线程的方式:
实现 Callable 接口
- 相较于实现 Runnable 接口的方式,方法可以有返回值,并且可以抛出异常
- 执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。FutureTask 是 Future 接口的实现类
/**
* 创建线程的方式三:实现 Callable 接口
* 相较于实现 Runnable 接口的方式,方法可以有返回值,并且可以抛出异常
*
* 执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。FutureTask 是 Future 接口的实现类
*/
public class TestCallable {
public static void main(String[] args) {
ThreadDemo2 td = new ThreadDemo2();
// 因为 Callable 接口有返回值,所以我们需要多一步接收返回值的操作
// 因此 Callable 接口需要一个 FutureTask 实现类的支持,用于接收运算结果
FutureTask<Integer> futureTask = new FutureTask<Integer>(td);
new Thread(futureTask).start(); // 启动线程
System.out.println("---------------");
// 在 new Thread(futureTask).start(); 线程的执行过程中,下面的主线程代码是没有执行的,子线程都执行完之后,下面主线程代码才会执行,类似于闭锁
try {
// 接收线程运算后的结果
Integer result = futureTask.get(); // futureTask 也可用于闭锁
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
/**
* 接口指定的泛型就是返回值的类型
*/
class ThreadDemo2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
/*class ThreadDemo2 implements Runnable {
@Override
public void run() {
}
}*/
用于解决多线程安全问题的方式:
synchronized : 隐式锁
- 同步代码块
- 同步方法
jdk1.5 以后
- 同步锁 Lock
- 注意:这是一个显示锁,需要通过 lock() 方法上锁,必须通过 unlock() 方法进行释放锁
public class TestLock {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket,"1号窗口").start();
new Thread(ticket,"2号窗口").start();
new Thread(ticket,"3号窗口").start();
}
}
class Ticket implements Runnable {
private int tickets = 100;
// lock 是一个接口,new 一个它的实现类
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock(); // 上锁
try {
if (tickets > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "余票为:" + --tickets);
}
} finally {
lock.unlock(); // 释放锁,这里的锁一定要释放,所以写到 finally 中
}
}
}
}
等待唤醒机制最经典一个的案例,就是生产者消费者案例
没有使用等待唤醒机制的生产者消费者案例:
/**
* 生产者消费者案例
* 生产者生产产品给店员
* 消费者从店员处取产品
*/
public class ProductorAndConsumer {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk);
Consumer consumer = new Consumer(clerk);
new Thread(productor, "生产者").start();
new Thread(consumer, "消费者").start();
}
}
class Clerk {
private int product = 0;
// 进货(假设最多只能有 10 个货物,超过 10 个就产品已满)
public synchronized void get() {
if (product >= 10) {
System.out.println("产品已满,无法添加");
} else {
System.out.println(Thread.currentThread().getName() + ":" + ++product);
}
}
// 卖货(如果货物小于 0 ,提示缺货)
public synchronized void sale() {
if (product <= 0) {
System.out.println("缺货了");
} else {
System.out.println(Thread.currentThread().getName() + ":" + --product);
}
}
}
/**
* 生产者
*/
class Productor implements Runnable {
// 生产者生产产品给到店员
private Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk.get();
}
}
}
/**
* 消费者
*/
class Consumer implements Runnable {
// 消费者从店员处消费商品
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk.sale();
}
}
}
运行结果:
生产者:1
生产者:2
生产者:3
生产者:4
生产者:5
生产者:6
生产者:7
生产者:8
生产者:9
生产者:10
产品已满,无法添加
产品已满,无法添加
产品已满,无法添加
产品已满,无法添加
产品已满,无法添加
产品已满,无法添加
产品已满,无法添加
产品已满,无法添加
产品已满,无法添加
产品已满,无法添加
消费者:9
消费者:8
消费者:7
消费者:6
消费者:5
消费者:4
消费者:3
消费者:2
消费者:1
消费者:0
缺货了
缺货了
缺货了
缺货了
缺货了
缺货了
缺货了
缺货了
缺货了
缺货了
产品已满了,生产者还没有停,一直在不断的生产,所以循环 20 次,会有 10 次产品已满的情况。消费者同理,货物被消费完了,但是消费者还在不断的消费,所以会打印 10 次 缺货了。
注:
在实际开发中,我们把添加和创建数据的线程叫生产者线程,把删除和销毁数据的线程叫消费者线程
如果生产者线程过快,也就是说你不断的发数据,另一方其实已经接收不到了,这时候就会造成一个数据丢失的情况
如果消费者线程过快, 也就是说你不断的接收数据,但是另一方其实已经不发了, 这时候就可能出现重复的数据或者错误的数据。
使用等待唤醒机制解决上述问题:
/**
* 生产者消费者案例
* 生产者生产产品给店员
* 消费者从店员处取产品
*/
public class ProductorAndConsumer {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk);
Consumer consumer = new Consumer(clerk);
new Thread(productor, "生产者").start();
new Thread(consumer, "消费者").start();
}
}
class Clerk {
private int product = 0;
// 进货(假设最多只能有 10 个货物,超过 10 个就产品已满)
public synchronized void get() {
if (product >= 10) {
System.out.println("产品已满,无法添加");
// 如果产品已满了,生产者就不能继续生产产品了
try {
this.wait(); // 等待
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + ":" + ++product);
// 如果生产者成功生产了一个产品,说明有多余的产品供消费者消费
this.notifyAll(); // 通知消费者可以进行消费
}
}
// 卖货(如果货物小于 0 ,提示缺货)
public synchronized void sale() {
if (product <= 0) {
System.out.println("缺货了");
// 如果缺货了,消费者就不能继续进行消费了
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + ":" + --product);
// 如果成功消费了一个产品,就可以通知生产者继续进行生产
this.notifyAll();
}
}
}
/**
* 生产者
*/
class Productor implements Runnable {
// 生产者生产产品给到店员
private Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk.get();
}
}
}
/**
* 消费者
*/
class Consumer implements Runnable {
// 消费者从店员处消费商品
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk.sale();
}
}
}
运行结果:
生产者:1
消费者:0
缺货了
生产者:1
生产者:2
生产者:3
生产者:4
生产者:5
生产者:6
生产者:7
生产者:8
生产者:9
生产者:10
产品已满,无法添加
消费者:9
消费者:8
消费者:7
消费者:6
消费者:5
消费者:4
消费者:3
消费者:2
消费者:1
消费者:0
缺货了
生产者:1
生产者:2
生产者:3
生产者:4
生产者:5
生产者:6
生产者:7
生产者:8
消费者:7
消费者:6
消费者:5
消费者:4
消费者:3
消费者:2
消费者:1
上述就是一旦发现产品已满,生产者就不会再继续生产了。一旦发现缺货,消费者就不会再继续消费了。
为了避免虚假唤醒,wait 操作应该始终写在循环中:
package com.iflytek.juc;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProductorAndConsumerForLock {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk);
Consumer consumer = new Consumer(clerk);
new Thread(productor, "生产者").start();
new Thread(consumer, "消费者").start();
}
}
class Clerk {
private int product = 0;
// 进货(假设最多只能有 10 个货物,超过 10 个就产品已满)
public void get() {
while (product >= 10) { // 为了避免虚假唤醒, wait 应该始终写在循环中
System.out.println("产品已满,无法添加");
// 如果产品已满了,生产者就不能继续生产产品了
try {
this.wait(); // 等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + ++product);
// 如果生产者成功生产了一个产品,说明有多余的产品供消费者消费
this.notifyAll(); // 通知消费者可以进行消费
}
// 卖货(如果货物小于 0 ,提示缺货)
public void sale() {
while (product <= 0) { // 为了避免虚假环境 wait 应该始终写在循环里面
System.out.println("缺货了");
// 如果缺货了,消费者就不能继续进行消费了
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + --product);
// 如果成功消费了一个产品,就可以通知生产者继续进行生产
this.notifyAll();
}
}
/**
* 生产者
*/
class Productor implements Runnable {
// 生产者生产产品给到店员
private Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk.get();
}
}
}
/**
* 消费者
*/
class Consumer implements Runnable {
// 消费者从店员处消费商品
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk.sale();
}
}
}
- Condition 接口描述了可能与锁有关联的条件变量。这些变量在用法上与使用 Object.wait() 类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同
- 在 Condition 对象中,与
wait
、notify
、notifyAll
方法对应的分别是await
、signal
、signalAll
- Condition 实例实际上是被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用
newCondition()
方法
/**
* 使用同步锁的方式避免线程安全问题
*/
public class ProductorAndConsumerForLock {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk);
Consumer consumer = new Consumer(clerk);
new Thread(productor, "生产者").start();
new Thread(consumer, "消费者").start();
}
}
class Clerk {
private int product = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
// 进货(假设最多只能有 10 个货物,超过 10 个就产品已满)
public void get() {
lock.lock();
try {
while (product >= 10) { // 为了避免虚假唤醒, wait 应该始终写在循环中
System.out.println("产品已满,无法添加");
// 如果产品已满了,生产者就不能继续生产产品了
try {
condition.await(); // 等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + ++product);
// 如果生产者成功生产了一个产品,说明有多余的产品供消费者消费
condition.signalAll(); // 通知消费者可以进行消费
}finally {
lock.unlock();
}
}
// 卖货(如果货物小于 0 ,提示缺货)
public void sale() {
lock.lock();
try {
while (product <= 0) { // 为了避免虚假环境 wait 应该始终写在循环里面
System.out.println("缺货了");
// 如果缺货了,消费者就不能继续进行消费了
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + --product);
// 如果成功消费了一个产品,就可以通知生产者继续进行生产
condition.signalAll();
} finally {
lock.unlock();
}
}
}
/**
* 生产者
*/
class Productor implements Runnable {
// 生产者生产产品给到店员
private Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk.get();
}
}
}
/**
* 消费者
*/
class Consumer implements Runnable {
// 消费者从店员处消费商品
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk.sale();
}
}
}
有这么一个功能需要完成,编写一个程序,开启三个线程,这三个线程的 id 分别是 A 、B、C ,每个线程将自己的 id 打印 5 遍,A 打印完 B打印,B 打印完 C 打印,然后循环 10 次,要求输出的结果必须按照顺序显示
/**
* 编写一个程序,开启三个线程,这三个线程的 id 分别为 A、B、C
* 每个线程将自己的 id 在屏幕上打印 5 遍,要求输出的结果必须按顺序显示,循环 10 轮
*/
public class TestABCAlternate {
public static void main(String[] args) {
AlternateDemo ad = new AlternateDemo();
new Thread(new Runnable() {
@Override
public void run() {
// 线程 A 在屏幕上打印 10 遍
for (int i = 1;i <= 10; i++) {
ad.loopA(i); // 第几轮打印 A
}
}
},"线程A").start();
new Thread(new Runnable() {
@Override
public void run() {
// 线程 B 在屏幕上打印 10 遍
for (int i = 1;i <= 10; i++) {
ad.loopB(i); // 第几轮打印 B
}
}
},"线程B").start();
new Thread(new Runnable() {
@Override
public void run() {
// 线程 C 在屏幕上打印 10 遍
for (int i = 1;i <= 10; i++) {
ad.loopC(i); // 第几轮打印 C
}
}
},"线程C").start();
}
}
class AlternateDemo {
// 当前正在执行线程的标记
private int number = 1;
// 访问上面 number 共享数据,得有锁,否则会存在线程安全问题
private Lock lock = new ReentrantLock();
// 三个线程要实现按序交替,一定是需要线程通信
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
/**
* 循环 A 线程
* totalLoop 循环第几轮
*/
public void loopA (int totalLoop) {
lock.lock();
try {
// 1.首先我需要判断当前执行的这个线程的编号是不是 1
if (number != 1) {
// 不等于 1 这个线程就不能操作,什么时候等于 1 我 A 线程才能进行打印操作
condition1.await(); // 不等于 1 就让它等待
}
// 2.等于 1 就执行打印操作,这里我打印 A 5次
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop );
// 线程名 + A打印的次数 + 这是第几轮打印的A
}
// 3.当 A 打印完之后就该把线程执行权交出去,唤醒别人,让别人去打
number = 2; // A打印完之后,让 number = 2,让 2 去执行
condition2.signal(); // 把 condition2 唤醒
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 循环 B 线程
* totalLoop 循环第几轮
*/
public void loopB (int totalLoop) {
lock.lock();
try {
// 1.首先我需要判断当前执行的这个线程的编号是不是 2
if (number != 2) {
// 不等于 2 这个线程就不能操作,什么时候等于 2 我 B 线程才能进行打印操作
condition2.await(); // 不等于 2 就让它等待
}
// 2.等于 2 就执行打印操作,这里我打印 B 5次
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop );
// 线程名 + B打印的次数 + 这是第几轮打印的B
}
// 3.当 B 打印完之后就该把线程执行权交出去,唤醒别人,让别人去打
number = 3; // B打印完之后,让 number = 3,让 3 去执行
condition3.signal(); // 把 condition3 唤醒
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 循环 C 线程
* totalLoop 循环第几轮
*/
public void loopC (int totalLoop) {
lock.lock();
try {
// 1.首先我需要判断当前执行的这个线程的编号是不是 3
if (number != 3) {
// 不等于 3 这个线程就不能操作,什么时候等于 3 我 C 线程才能进行打印操作
condition3.await(); // 不等于 3 就让它等待
}
// 2.等于 3 就执行打印操作,这里我打印 C 5次
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i + "\t" + totalLoop );
// 线程名 + B打印的次数 + 这是第几轮打印的B
}
// 3.当 C 打印完之后就该把线程执行权交出去,唤醒别人,让别人去打
number = 1; // C打印完之后,让 number = 1,让 1 去执行
condition1.signal(); // 把 condition1 唤醒
System.out.println("------------");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
- ReadWriteLock 维护了一对相关的锁,一个用于只读操作,一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是读占的。
- ReadWriteLock 读取操作通常不会改变共享资源,但执行写入操作时,必须独占方式来获取锁。对于读取操作占多数的数据结构,ReadWriteLock 能够提供比独占锁更高的并发性。而对于只读的数据结构,其中包含的不变性可以完全不需要考虑加锁操作
说明:
原来我们有线程安全问题,就直接上锁,所以不管是读还是写,一次只能有一个线程,效率比较低。现在可以把它分开,写一次只能有一个线程,但读可以有 n 个线程一起读,因此只要你读操作比较多的话,就可以提高效率。
什么情况下用读写锁?
写写/读写 需要互斥
① 多个线程都在写,那么这几个线程肯定要互斥
②一个线程在写一个线程在读,这两个线程也需要互斥,假如说一个线程写到一半就被读了,这样是有问题的,因此需要互斥
读读 不需要互斥
所以现在我们可以不用像原来那样,只要有线程安全问题,就上锁,不管读还是写只能一个线程独占
public class TestReadWriteLock {
public static void main(String[] args) {
// 我用一个线程去写,10个线程去读
ReadWriteLockDemo rw = new ReadWriteLockDemo();
new Thread(new Runnable() {
@Override
public void run() {
rw.set(888);
}
},"writeLock").start();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
rw.get();
}
}).start();
}
}
}
class ReadWriteLockDemo {
private int number = 0;
// 读和写访问的都是共享数据 number,存在线程安全问题
// 现在我想读可以允许多线线程并发的读,写一次只能允许一个线程去写
private ReadWriteLock lock = new ReentrantReadWriteLock();
// 读操作
public void get() {
lock.readLock().lock(); // 上锁
try {
System.out.println(Thread.currentThread().getName() + ":" + number);
} finally {
lock.readLock().unlock(); // 释放锁
}
}
// 写操作
public void set(int number) {
lock.writeLock().lock(); // 上锁
try {
System.out.println(Thread.currentThread().getName());
this.number = number;
} finally {
lock.writeLock().unlock(); // 释放锁
}
}
}
判断下面八种情况,打印的 "one" "two" 情况
/**
* 判断打印 "one" "two"
*
* 1、两个普通同步方法,两个线程,标准打印,打印? one two
* 2、新增 Thread.sleep() 方法给 getOne() 方法,打印? one two
* 3、新增普通方法 getThree() ,打印? three one two
* 4、两个普通的同步方法,两个 Number 对象,打印? two one
* 5、修改 getOne 为静态同步方法,一个 Number 对象,打印? two one
* 6、修改两个方法均为静态同步方法,一个 Number 对象,打印? one two
* 7、一个静态同步方法,一个非静态同步方法,两个 Number 对象,打印? two one
* 8、两个静态同步方法,两个 Number 对象,打印? one two
*
* 线程八锁的关键:
* ① 非静态方法的锁默认为 this ,静态方法的锁为对应的 Class 实例
* ② 某一个时刻内,只能有一个线程持有锁,无论几个方法
*/
public class TestThread8Monitor {
public static void main(String[] args) {
Number number = new Number();
Number number2 = new Number();
new Thread(new Runnable() {
@Override
public void run() {
number.getOne();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
number2.getTwo();
}
}).start();
/*new Thread(new Runnable() {
@Override
public void run() {
number.getThree();
}
}).start();*/
}
}
class Number {
public static synchronized void getOne() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public static synchronized void getTwo() {
System.out.println("two");
}
public void getThree() {
System.out.println("three");
}
}
线程八锁的关键:
① 非静态方法的锁默认为 this ,静态方法的锁为对应的 Class 实例
② 某一个时刻内,只能有一个线程持有锁,无论几个方法
class Number {
public static synchronized void getOne() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public static synchronized void getTwo() {
System.out.println("two");
}
// 以上面这两个方法为例,这两个都是静态同步方法,锁是谁? 答案是,Number.class ,这个 Class 实例只有一个,两个线程同时执行,一旦被某个线程抢到了执行权,就握住了这把锁,另外一个线程就抢不到锁,就得阻塞,等那个线程把锁释放了,另外一个线程才能进得去(所以关键要找对锁的对象)
}
//--------------------------------------------------------------------
class Number {
public static synchronized void getTwo() {
System.out.println("two");
}
public void getThree() {
System.out.println("three");
}
// 再以上面这两个方法说下,上面这两个方法一个是静态同步方法,一个是普通方法,这两个方法是没有竞争关系,不存在一个线程,另一个线程就阻塞,等线程释放才能执行,这两个线程可以同时执行
}
//--------------------------------------------------------------------
class Number {
public static synchronized void getOne() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("one");
}
public synchronized void getTwo() {
System.out.println("two");
}
// 再以上面这两个方法为例,一个是静态同步方法,一个是非静态同步方法,静态同步方法的锁是 Number.class 实例,非静态同步方法的锁是 this ,这两个也不存在竞争关系,所以两个线程可以同时执行,就不存在线程1抢到了锁,线程2 必须等线程1 释放锁后才能执行,可以同时执行。
}
线程池提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁线程的额外开销,提高了响应的速度。
1)、首先我们要知道为什么要使用线程池?
public static void main(String[] args) { ThreadPoolDemo tpd = new ThreadPoolDemo(); new Thread(tpd).start(); new Thread(tpd).start(); }以前我们需要使用一个线程的时候,会去
new Thread()
然后启动线程。每次需要用线程的时候我都需要创建一个线程,然后给线程分配任务,然后再启动,当这个线程应用完之后又需要把它销毁掉。如果你任务过多,你去频繁的创建或销毁线程,这样是非常消耗资源的。
2)、线程池的体系结构:
java.util.concurrent.Executor:负责线程的使用和调度的根接口
|--ExecutorService:子接口,线程池的主要接口
|--ThreadPoolExecutor:线程池的实现类
|--ScheduledExecutorService:子接口,负责线程的调度
|--ScheduledThreadPoolExecutor:继承 ThreadPoolExecutor,实现 ScheduledExecutorService
3)、工具类(Executors
):
ExecutorService newFixedThreadPool()
:创建固定大小的线程池ExecutorService newCachedThreadPool()
:缓存线程池ExecutorService newSingleThreadExecutor()
:创建单个线程池。线程池中只有一个线程。ScheduledExecutorService newScheduledThreadPool()
:创建固定大小的线程,可以延迟或定时的执行任务
// ------------------------ 参数使用 Runnable-------------------------------------
public class TestThreadPool {
public static void main(String[] args) {
// 1.创建线程池,线程池里面放 5 个线程
ExecutorService pool = Executors.newFixedThreadPool(5);
ThreadPoolDemo tpd = new ThreadPoolDemo();
// 2.为线程池中的线程分配任务
for (int i = 0; i < 10; i++) { // 10 个线程一起跑
pool.submit(tpd);
}
// 3.关闭线程池
pool.shutdown();
}
}
class ThreadPoolDemo implements Runnable {
private int i = 0;
@Override
public void run() {
while (i < 100) {
i++;
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
// ------------------------ 参数使用 Callable-------------------------------------
public class TestThreadPool {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1.创建线程池,线程池里面放 5 个线程
ExecutorService pool = Executors.newFixedThreadPool(5);
List<Future<Integer>> list = new ArrayList<Future<Integer>>();
for (int i = 0; i < 10; i++) {
// 2.为线程池中的线程分配任务
Future<Integer> future = pool.submit(new Callable<Integer>() {
// 跑了 10 个线程,就有 10 个返回值,用 Future 来接收返回值
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
});
list.add(future);
}
for (Future future : list) {
System.out.println(future.get());
}
// 3.关闭线程池
pool.shutdown();
}
}
public class TestScheduledThreadPool {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建有 5 个线程的线程池,并且这个线程池可以完成任务调度
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 4 个线程一起跑
for (int i = 0; i < 4; i++) {
ScheduledFuture<Integer> future = scheduledThreadPool.schedule(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int number = new Random().nextInt(100);
System.out.println(Thread.currentThread().getName() + ":" + number);
return number;
}
}, 1, TimeUnit.SECONDS); // 延迟 1 秒执行
System.out.println(future.get());
}
scheduledThreadPool.shutdown();
}
}
- Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总。
public class TestForkJoinPool {
public static void main(String[] args) {
// ForkJoin 框架需要 ForkJoinPool 的支持
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinSumCalculate(0L, 100000000L);
Long result = pool.invoke(task);
System.out.println(result);
}
}
class ForkJoinSumCalculate extends RecursiveTask<Long> {
private long start;
private long end;
private static final long THRESHOLD = 10000L; // 临界值
public ForkJoinSumCalculate(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long length = end - start;
if (length <= THRESHOLD) { // 小于等于临界值,直接计算和
long sum = 0L;
for (long i = start; i < end; i++) {
sum += i;
}
return sum;
} else {
// 进行拆分
long middle = (start + end) / 2;
ForkJoinSumCalculate left = new ForkJoinSumCalculate(start,middle);
left.fork(); // 进行拆分,同时压入线程队列
ForkJoinSumCalculate right = new ForkJoinSumCalculate(middle + 1, end);
right.fork();
return left.join() + right.join();
}
}
二刷
JUC 是 java.util.concurren
工具包的简称。是一个处理线程的工具包,在 JDK1.5 的时候出现的。
进程:系统中正在运行的一个应用程序,程序一旦运行就是进程,进程是资源分配的最小单位。
线程:进程之内独立执行的一个单元执行流。线程是程序执行的最小单元。
- sleep 方法是 Thread 类中的方法,wait 方法是 Object 中的方法
- sleep 方法不会释放锁