1、JUC简单介绍
-
JUC是java.util .concurrent工具包的简称,是一个处理线程的工具包。
-
在Java的Concurrent包下还有atomic包和locks包,分别是原子性和各种锁。
-
在线程的学习方面,我们了解到Thread和Runnable,通过这两个东西便可以实现线程的调用。之所以使用JUC,是因为JUC能够获取返回值而且Callable的执行效率更高。
2、线程 & 进程
- 进程
狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
- 线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
Windows系统下,在任务管理器 -> 性能中,就可以看到当前系统运行的线程数和进程数。
Java默认线程——Main线程、GC线程。
Java自身不能开启线程,所谓开启线程,实际上是调用native本地方法来启动一个线程,而native方法并非由Java代码实现。Java自身运行在虚拟机上,无法直接操作硬件,便会调用native本地方法(主要是C/C++)间接操作硬件。
new Thread(demo).start(); //常用的开启线程的方法
//start源码的主要内容
public synchronized void start() {
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {}
}
start是将任务add到一个group里,然后附加初始值false,并尝试调用start0方法开启任务的执行。
private native void start0();
而start0是一个native方法。
3、并发 & 并行
- 并发
并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
指CPU的一核,利用高速交替的方法,模拟出多线程的效果。
- 并行
在操作系统中是指,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。
指多核CPU,同时运行多个线程。
并发编程的目的是 提高CPU的利用与,充分使用CPU的资源
在Java语言中,线程拥有6个状态,可以直接点入Thread.State查看源码获得——NEW
,RUNNABLE
,BLOCKED
, WAITING
,TIMED_WAITING
,TERMINATED
;
wait与sleep的区别
- wait来自Object类,sleep来自Thread类
- wait会释放锁,sleep不会释放
- wait必须在同步代码块中,sleep可以在任意处
4、synchronized
假如我们设计一个售票处,基于OOP思想设计一个资源类,然后new多个线程执行这个类,从而完成售票操作。
Main方法
public static void main(String[] args) {
ticketSale ticketSale = new ticketSale();
new Thread(() -> {
for (int i = 0; i < 60; i++) {
ticketSale.sale();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 60; i++) {
ticketSale.sale();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 60; i++) {
ticketSale.sale();
}
}).start();
}
ticketSale类
static class ticketSale{
private int num = 100;
public void sale(){
if(num > 0){
System.out.println(Thread.currentThread().getName() + "卖出了1张票" + ",剩余:" + (--num) + "张.");
}
}
}
按照正常思路,应当是一张一张卖掉逐步归零。而实际打印结果是:
这是最后几张票的售卖情况,可以明显看到Thread1和Thread2打印的数据存在严重的错误。原因是存在3个线程争夺资源,举个例子:
现在有100张票,ABC三个人同时开始售卖,同时取到值100,然后A卖了5张、B卖了3张、C卖了1张,三次修改后应该还剩91张。而因为可见性问题,ABC均 取100进行计算并重新写入。导致A写入95,但是B取到的是100,所以B重新写入变成了97,C取到的是100,于是C重写成了99。这就是所谓的ABA问题。解决问题的传统方法就是使用synchronized锁,确保资源单独执行。
重写ticketSale类(中的sale方法)
static class ticketSale{
private int num = 100;
public synchronized void sale(){
if(num > 0){
System.out.println(Thread.currentThread().getName() + "卖出了1张票" + ",剩余:" + (--num) + "张.");
}
}
}
输出结果便与设想中的相同:
5、Lock
官方文档:
Lock
实现提供比使用synchronized
方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition
。锁是用于通过多个线程控制对共享资源的访问的工具。 通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。 但是,一些锁可能允许并发访问共享资源,如
ReadWriteLock
的读锁。使用
synchronized
方法或语句提供对与每个对象相关联的隐式监视器锁的访问,但是强制所有锁获取和释放以块结构的方式发生:当获取多个锁时,它们必须以相反的顺序被释放,并且所有的锁都必须被释放在与它们相同的词汇范围内。随着这种增加的灵活性,额外的责任。 没有块结构化锁定会删除使用
synchronized
方法和语句发生的锁的自动释放。 在大多数情况下,应使用以下惯用语:Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }
当在不同范围内发生锁定和解锁时,必须注意确保在锁定时执行的所有代码由try-finally或try-catch保护,以确保在必要时释放锁定。
根据官方要求模式重写ticketSale类(中的sale方法)
static class ticketSale{
private int num = 100;
//new锁
ReentrantLock lock = new ReentrantLock();
public void sale(){
//上锁
lock.lock();
try {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了1张票" + ",剩余:" + (--num) + "张.");
}
}finally {
//在finally中释放锁
lock.unlock();
}
}
}
synchronized与lock的区别
- synchronized是Java关键字,lock是一个类
- synchronized无法获取锁的状态,lock可以
- synchronized会自动释放锁,Lock需要手动释放
- synchronized获取锁后会进行阻塞,而lock可以使用trylock尝试获取锁
- synchronized可重入锁,不可中断,非公平锁;lock可重入锁,可以判断锁,可以自己设置是否公平
- synchronized适合锁少量同步代码,lock适合锁大量同步代码
6、生产者 & 消费者
6.1、synchronized中的问题
我们编写一个资源类,分别包含自加1和自减1,并且使用synchronized进行保护。然后在Main中开4个线程分别执行2个自加2个自减
资源类:
static class data{
private int a = 0;
public synchronized void increament() throws InterruptedException {
if(a != 0) {
this.wait();
}
a++;
System.out.println(Thread.currentThread().getName() + ": " + a);
this.notify();
}
public synchronized void decreament() throws InterruptedException {
if(a == 0) {
this.wait();
}
a--;
System.out.println(Thread.currentThread().getName() + ": " + a);
this.notify();
}
}
Main:
public static void main(String[] args) {
data dt = new data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
dt.decreament();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
dt.decreament();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
dt.increament();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
dt.increament();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
按照思路应当是,4个线程分别调用自加自减,当i为0时自减,不为0时自加。并且因为有synchronized保护,程序应当不断输出-1和0,然而实际上:
原因:if语句只能判断1次,当出现多个符合条件的参数时,程序也只进行了一次。
官方文档:
public final void wait(){} throws InterruptedException
导致当前线程等待,直到另一个线程调用该对象的
notify()
方法或notifyAll()
方法。 换句话说,这个方法的行为就好像简单地执行呼叫wait(0)
。当前的线程必须拥有该对象的显示器。 该线程释放此监视器的所有权,并等待另一个线程通知等待该对象监视器的线程通过调用
notify
方法或notifyAll
方法notifyAll
。 然后线程等待,直到它可以重新获得监视器的所有权并恢复执行。像在一个参数版本中,中断和虚假唤醒是可能的,并且该方法应该始终在循环中使用:
synchronized (obj) { while (<condition does not hold>) obj.wait(); ... // Perform action appropriate to condition }
在资源类中将if换成while即可解决问题
6.2、Lock中的问题
资源类:
static class data{
private int a = 0;
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increament() throws InterruptedException {
lock.lock();
try {
while(a != 0) {
condition.await();
}
a++;
System.out.println(Thread.currentThread().getName() + ": " + a);
condition.signalAll();
} finally {
lock.unlock();
}
}
public void decreament() throws InterruptedException {
lock.lock();
try {
while (a == 0) {
condition.await();
}
a--;
System.out.println(Thread.currentThread().getName() + ": " + a);
condition.signalAll();
}finally {
lock.unlock();
}
}
}
利用condition可以实现等待和唤醒,除此之外,condition可以实现精准的通知和线程唤醒
7、使用Condition进行精准通知和唤醒
在多线程的时候,我们可以让线程交替运行,避免某个线程抢夺大量资源导致其他线程浪费。
我们可以定义3个condition用来管理3个线程,用一个flag标识管理先后顺序。
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int flag = 1;
public void printA(){
lock.lock();
try {
while(flag != 1){
condition1.await();
}
System.out.println("A已执行");
flag = 2;
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while(flag != 2){
condition1.await();
}
System.out.println("B已执行");
flag = 3;
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while(flag != 3){
condition1.await();
}
System.out.println("C已执行");
flag = 1;
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
这样即可使得A、B、C交替运行。
参考文献
线程-百度百科 https://baike.baidu.com/item/%E7%BA%BF%E7%A8%8B/103101
进程-百度百科 https://baike.baidu.com/item/%E8%BF%9B%E7%A8%8B/382503