并发和并行
并发: 多个线程抢一份资源。比如说12306 抢票。
并行:泡方便面。正常追求效率的情况下,撕调料包的情况下,烧热水。比如说一个人执行了多个任务,在听歌的时候走路。
关于两者的区别关注下面的这个连接:
并发会引发的问题,(线程的安全问题)说个窗口卖票的问题:
package com.isea.mybatis;
class Tickets{
private int sharData = 10;
public void sale(){
while (true){
if (sharData <=0){
break;
}else {
try {
Thread.sleep(300);
sharData --;
if (sharData == 0){
System.out.println(Thread.currentThread().getName() + "卖出最后一张票");
}else {
System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩" + sharData );
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public class TestTickets{
public static void main(String[] args) {
Tickets tickets = new Tickets();
for (int i = 0; i < 3 ; i ++){
new Thread(() ->{tickets.sale();},"windows" + (i + 1)).start();
}
}
}
运行的结果如下:
windows1卖出一张票,还剩9
windows2卖出一张票,还剩9
windows3卖出一张票,还剩9
windows1卖出一张票,还剩7
windows2卖出一张票,还剩7
windows3卖出一张票,还剩7
windows3卖出一张票,还剩6
windows1卖出一张票,还剩6
windows2卖出一张票,还剩6
windows3卖出一张票,还剩4
windows1卖出一张票,还剩5
windows2卖出一张票,还剩5
windows1卖出一张票,还剩2
windows3卖出一张票,还剩1
windows2卖出一张票,还剩2
windows1卖出最后一张票
windows3卖出一张票,还剩-2
windows2卖出一张票,还剩-2
卖出重票和负票,为什么会出现这样的结果呢?我们想一下这样的场景:
窗口1(线程1)先抢到CPU的使用权,线程1开始调用sale方法,判断sharData是否小于等于0,不满足,sleep300毫秒,让出CPU的使用权,线程2抢到CPU的使用权, 判断之后,也sleep300毫秒,让出CPU的使用权,线程3抢到CPU资源,判断之后sleep300毫秒,这三个线程都保持初始的时候sharData值10,此时线程1醒来,sharData --,打印9张,接着线程2醒来,sharData --,打印9张,接着线程3醒来,sharData --,打印9张,错误就是这样产生的,所以我们希望有一种机制能够将我们共享的资源加上锁,当某一个线程操作这个共享的资源的时候是无人打扰的,就好像上卫生间的时候,我们要拉上门栓。
方法的同步能够做到这点,方法同步也叫给资源加锁,使用 synchronized 或者是 jus Lock ,下面是一个演示:
package com.isea.java;
class Tickets{
private int sharData = 10;
public synchronized void sale(){
while (true){
if (sharData == 0){
break;
}else {
try {
Thread.sleep(300);
sharData --;
if (sharData == 0){
System.out.println(Thread.currentThread().getName() + "卖出最后一张票");
}else {
System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩" + sharData );
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public class TestTickets{
public static void main(String[] args) {
Tickets tickets = new Tickets();
for (int i = 0; i < 3 ; i ++){
new Thread(() ->{tickets.sale();},"windows" + (i + 1)).start();
}
}
}
接下来,我们使用JUC和工程化代码来解决这个问题。
JUC:java并发包处理多线程(工程)
java.util.concurrent ,包,
线程操作资源类,高内聚低耦合。
对资源操作的全部方法永远写在自己身上,让资源类紧紧地带着这些方法,俗称高内聚,这些对资源操作的方法不能写在调用者的身上,即解耦合。
Interface Lock
public class ReentrantLock implements Lock, java.io.Serializable {}
// ReentrantLock 可重用锁
//Lock implementations provide more extensive locking operations than can be obtained using synchronized methods and statements
下面是工程化的代码:
package com.isea.java;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Tickets{
private int sharData = 10;
private Lock lock = new ReentrantLock();
public void sale() {
lock.lock();
try {
if (sharData > 0){
System.out.println(Thread.currentThread().getName() + "卖出第:\t" + sharData -- + "\t 还剩下:" + sharData);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class TestTickets{
public static void main(String[] args) {
Tickets tickets = new Tickets();
new Thread(() -> {for (int i = 0; i < 40; i++) { tickets.sale();}},"A").start();
new Thread(() -> {for (int i = 0; i < 40; i++) { tickets.sale();}},"B").start();
new Thread(() -> {for (int i = 0; i < 40; i++) { tickets.sale();}},"C").start();
}
}
结果:
A卖出第: 10 还剩下:9
A卖出第: 9 还剩下:8
A卖出第: 8 还剩下:7
A卖出第: 7 还剩下:6
A卖出第: 6 还剩下:5
A卖出第: 5 还剩下:4
A卖出第: 4 还剩下:3
A卖出第: 3 还剩下:2
A卖出第: 2 还剩下:1
A卖出第: 1 还剩下:0
刚开始,我们都是使用synchronize来加锁的,但是现在我们使用了Lock来加锁,为甚会出现Lock呢?
当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况: 1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有; 2)线程执行发生异常,此时JVM会让线程自动释放锁。 那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
两者之间的区别:
1. lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现; 2. synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。) 3. lock等待锁过程中可以用interrupt来终端等待,而synchronized只能等待锁的释放,不能响应中断;
关于并发的出现线程安全的问题,还可以看看ArrayList,HashSet , HashMap的线程不安全,和对应的解决办法:
卖票的时候,是多个线程对同一个资源纵向的抢夺,只需要加上一把锁即可,但是对于生产者和消费者问题,不仅有纵向的争夺,还有横向的线程之间的交流和通信,即生产者在生产的时候需要看生产的产品是都被消费了,而消费者在消费的时候需要看生产者有没有把产品生产出来。
两个线程,可以操作初始值为零的一个变量,实现一个线程对变量加1,一个线程对变量减1,交替10轮?
这里面面临的问题是:生产者怎么知道该自己生产了,消费者怎么知道该自己消费了
wait和sleep的区别?
他们两个都能够让线程进入阻塞的状态,但是wait会释放锁(对于资源类的锁),即会释放资源;sleep是睡,但是不会释放锁,会将锁对应的资源牢牢地攥在自己的手中,只是会让出CPU,让别的线程获得执行。更多区别在下面:
下面是代码:
package com.isea.java;
/**
* 两个线程,可以操作初始值为零的一个变量,实现一个线程对变量加1,一个线程对变量减1,交替10轮
* 1,线程 操作 资源类 高内聚低耦合
* 2,判断 干活 唤醒
*/
class ShareData{
private int number = 0;
public synchronized void increment() throws InterruptedException {
if (number != 0){
this.wait();
}
number ++;
System.out.println(Thread.currentThread().getName() + ":\t" + number);
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
if (number == 0){
this.wait();
}
number --;
System.out.println(Thread.currentThread().getName() + ":\t" + number);
this.notifyAll();
}
}
public class ProdConsumerDemo {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
shareData.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
shareData.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
执行结果:
A: 1
B: 0
A: 1
B: 0
A: 1
B: 0
A: 1
B: 0
A: 1
B: 0
A: 1
B: 0
A: 1
B: 0
A: 1
B: 0
A: 1
B: 0
A: 1
B: 0
假设现在有两个生产者,两个消费者:
public class ProdConsumerDemo {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
shareData.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
shareData.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
shareData.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
shareData.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
结果打印:
A: 1
C: 2
D: 1
D: 0
C: 1
A: 2
C: 3
D: 2
结果发现出现了2,3这样的数据,为什么?这就是java多线程中的虚假唤醒。下飞机之后,上飞机必须再次过安检。下面说一下虚假唤醒,
即只有满足一定的条件之后,线程才能醒过来,但是在不满足条件的情况下醒过来了,就叫做虚假唤醒。但是在只有一个生产者和只有一个消费者的情况下并不会存在虚假唤醒 ,这时候只能被对方唤醒,相当线程是串行的,下图帮助理解:
如果是多个生产者和消费者的话,情况是这样的:
如上图中的内容,某一个时刻,count的数量为0 ,线程1和线程2都进入等待的状态,这事线程2生产一个,同时唤醒所有的线程,由于线程1和线程2是if判断,只会进行一次,其中一个线程进行消费操作,但是另外一个线程并不会再次进行判断,任然会继续消费,虚假唤醒导致线程不安全。
面:请说出Object中五个常用的方法:toString() hashCode() wait() notify() notifyAll() ,equals() , 正是因为wait()是Object中的方法,所以每一个对象都会有一把对象锁,看一下wait的官方文档:
The current thread must own this object's monitor
As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}
即,
在进行多线程通信判断的时候,不能用if 只能用while
醒来之后,拉回来在进行一次安检,这就是多线程通信中虚假唤醒的解决办法。
现在修改:
/**
* 两个线程,可以操作初始值为零的一个变量,实现一个线程对变量加1,一个线程对变量减1,交替10轮
* 1,线程 操作 资源类 高内聚低耦合
* 2,判断 干活 唤醒
* 3,虚假唤醒
*/
class ShareData {
private int number = 0;
public synchronized void increment() throws InterruptedException {
while (number != 0) {
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + ":\t" + number);
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
while (number == 0) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + ":\t" + number);
this.notifyAll();
}
}
我们再次回过头来,思考这样的一个问题,为什么会存在2,3这样的情况呢?上文说到这是由于多线程通信的时候虚假唤醒问题,这是一个什么问题呢?
我们现在有A,B,C,D四个线程,A,C 生产,B,D消费。某时刻,B,C,D都在等待,number 为 0 ,A获得CPU,并对资源加锁,number 变成1,唤醒所有线程,此时,A,自己又抢到了CPU的执行权,判断之后做了wait,释放了锁,此时C抢到了CPU时间片,对资源加锁,number 变成了2,唤醒所有,也唤醒了A,A继续抢到了CPU,number变成了3。
现在,我们用JUC的写法来重现一下这个程序:
public interface ConditionCondition factors out the Object monitor methods (wait, notify and notifyAll) into distinct objects
to give the effect of having multiple wait-sets per object, by combining them with the use of arbitrary Lock implementations.
Where a Lock replaces the use of synchronized methods and statements, a Condition replaces the use of the Object monitor methods.
package com.isea.java;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 两个线程,可以操作初始值为零的一个变量,实现一个线程对变量加1,一个线程对变量减1,交替10轮
* 1,线程 操作 资源类 高内聚低耦合
* 2,判断 干活 唤醒
* 3,虚假唤醒
*/
class ShareData {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + ":\t" + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + ":\t" + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ProdConsumerDemo {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
shareData.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
shareData.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
shareData.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
shareData.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
最后关于多线程操作的口诀:
* 两个线程,可以操作初始值为零的一个变量,实现一个线程对变量加1,一个线程对变量减1,交替10轮
* 1,线程 操作 资源类 高内聚低耦合
* 2,判断 干活 唤醒
* 3,虚假唤醒