Java-多线程-3
学习视频:B站 狂神说Java – https://www.bilibili.com/video/BV1V4411p7EF
学习博客:csdn – https://blog.youkuaiyun.com/qijingwang/article/details/104892717
学习博客:csdn – https://blog.youkuaiyun.com/yalu_123456/article/details/91050036?spm=1001.2014.3001.5501
8、线程同步规则
8.1、线程简介及多线程的影响
我们先回顾一下线程。
线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个程序至少有一个进程。线程是CPU调度和执行的单位。
一个进程中会有一个或多个线程,这些线程会共享进程的资源,但又有各自的程序计数器、栈和局部变量表。同一进程中的不同线程能够访问相同的变量,并且在同一个堆上分配对象。这样就会产生不同的正负作用影响。
优势(正作用):
- 提高程序的运行性能。
- 充分利用系统的处理能力,提高系统的资源利用率。
- 提高系统响应性,即线程可以在运行现有任务的情况下立即开始处理新的任务。
问题(负作用):
- 安全性问题:即并发问题,共享进程资源,访问相同变量,却有各自的程序计数器、栈和局部变量表, 这会造成对同一对象分配处理时,状态变量改变了 或者 没有及时改变,发生数据紊乱现象。
- 活跃性问题:不正确的加锁、解锁方式可能会导致死锁or活锁问题。
- 性能问题:多线程并发即多个线程切换运行,线程切换会有一定的消耗并且不正确的加锁。
8.2、线程同步规则
并发
并发:同一个对象被多个线程同时操作。 这样造成了数据紊乱。如下图例:
线程同步
现实生活中,我们会遇到 ” 同一个资源 , 多个人都想使用 ” 的问题 , 比如,食堂排队打饭 , 每个人都想吃饭 , 最天然的解决办法就是 , 排队 . 一个个来.
这种所有人都去共享 饭资源,就好比是多线程。 在处理多线程并发问题时,我们可以借鉴排队打饭这种方法。 多个线程访问同一个对象 , 并且某些线程还想修改这个对象 .这时候我们就需要线程同步 . 线程同步其实就是一种等待机制 , 多个需要同时访问此对象的线程进入这个对象的等待池 形成队列, 等待前面线程使用完毕 , 下一个线程再使用。
线程同步,队列和锁:由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题。为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:
-
一个线程持有锁会导致其它所有需要此锁的线程挂起;
-
在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
-
如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。
多个线程访问同一对象,造成并发问题。
队列和锁 才能去完成线程同步,解决线程并发的安全问题,但是性能就会差。
9、三大不安全案例
9.1、不安全的买票案例
在买票的时候, 多个线程去对买票这个进程中的共享资源 票数 进行共享,但是由于每个线程有各自的程序计数器、虚拟机栈和本地方法栈、局部变量表,导致数据资源改变,状态不一致,造成了数据紊乱。
我们采用sleep()方法模拟延时,放大问题的发生性。
例子:不安全的买票,代码:
package com.AL.Multithread;
// 线程不安全,数据紊乱。
// 不安全的买票。 会出现票数为负
public class UnsafeBuyTicket {
public static void main(String[] args){
BuyTicket station = new BuyTicket();
new Thread(station,"可怜的我").start();
new Thread(station,"得意的你").start();
new Thread(station,"可恶的黄牛党").start();
}
}
class BuyTicket implements Runnable{
// 票数
private int ticketNums = 10;
boolean flag = true; // 外部停止方式,标志位
@Override
public void run() {
// 买票
while (flag){
try {
buy();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 买票,这一部分是为了去改变 flag,便于线程停止
private void buy() throws InterruptedException{
// 判断是否有票
if (ticketNums <= 0){
flag = false;
return;
}
//模拟延时。 可以放大 问题的发生性
Thread.sleep(100);
//买票
System.out.println(Thread.currentThread().getName()+"拿到了"+ticketNums--);
}
}
结果:票数出现重复,有时为负数。这种错误的原因是:每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。 这就是线程不安全的一个例子。
得意的你拿到了8
可怜的我拿到了9
可恶的黄牛党拿到了10
可怜的我拿到了7
得意的你拿到了5
可恶的黄牛党拿到了6
得意的你拿到了4
可恶的黄牛党拿到了4
可怜的我拿到了3
得意的你拿到了2
可怜的我拿到了1
可恶的黄牛党拿到了2
9.2、不安全的取钱案例
不安全的取钱案例: 并发问题,数据紊乱,线程不安全。
对于银行这个类来讲,会有账户;会有两个人去取钱,即对应着 多个线程对同一个资源对象进行操作。而这两个人即两个线程会由于各自本身具有程序计数器,内存空间,导致这同一个对象分别在两个线程中的状态变量不能同步更新, 从而造成数据紊乱。
代码:
package com.AL.Multithread;
//不安全的取钱
// 两个人去银行取钱, 账户
public class UnsafeBank {
public static void main(String[] args){
//账户
Account account = new Account(100,"结婚基金");
Drawing you=new Drawing(account,50,"你");
Drawing girlFriend=new Drawing(account,100,"女朋友");
you.start();
girlFriend.start();
}
}
//账户
class Account{
int money; //余额
String name; //卡名
public Account(int money, String name){
this.money = money;
this.name = name;
}
}
//银行: 模拟取款
class Drawing extends Thread{
Account account; //账户
int drawingMoney; //取了多少钱
int nowMoney; // 现在卡里还有多少钱
public Drawing(Account account, int drawingMoney, String name){
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
//取钱
@Override
public void run() {
//判断有没有钱
if (account.money - drawingMoney < 0){
System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
return;
}
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
// 卡内余额 = 余额-你取的钱
account.money = account.money - drawingMoney;
// 你手里的钱
nowMoney= nowMoney + drawingMoney;
System.out.println(account.name+"余额为:"+account.money);
System.out.println(this.getName()+"手里的钱:"+nowMoney);
}
}
结果:这出现错误,账户金额为负数。
结婚基金余额为:-50
结婚基金余额为:-50
女朋友手里的钱:100
你手里的钱:50
9.3、不安全的集合案例
例子:
package com.AL.Multithread;
import java.util.ArrayList;
import java.util.List;
public class UnSafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 100000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
for (int i = 5;i>0;i--){
Thread.sleep(1000);
System.out.println("倒计时"+i);
}
System.out.println(list.size());
}
}
结果: 结果中没有到达100000,线程不安全。
倒计时5
倒计时4
倒计时3
倒计时2
倒计时1
99974
10、同步方法及同步块
10.1、同步方法:
同步方法:
-
由于我们可以通过 private 关键字来保证数据对象只能被方法访问 , 所以我们只需要针对方法提出一套机制 , 这套机制就是 synchronized 关键字 , 它包括两种用法 :synchronized 方法 和synchronized 块 。
同步方法 : public synchronized void method(int args) {} -
synchronized方法控制对 “对象” 的访问 , 每个对象对应一把锁 , 每个synchronized方法都必须获得调用该方法的对象的锁才能执行 , 否则线程会阻塞 ;
方法一旦执行 , 就独占该锁 , 直到该方法返回才释放锁 , 后面被阻塞的线程才能获得这个锁 , 继续执行;
缺陷 : 若将一个大的方法申明为synchronized 将会影响效率
同步方法的弊端:
方法里面需要修改的内容才需要锁,
锁的太多 , 浪费资源
10.2、同步块:
-
同步块 : synchronized (Obj ) { }
-
Obj 称之为 同步监视器
- Obj 可以是任何对象 , 但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器 , 因为同步方法的同步监视器就是this , 就是这个对象本身 , 或者是 class [ 反射中讲解 ]
-
同步监视器的执行过程
- 第一个线程访问 , 锁定同步监视器 , 执行其中代码 .
- 第二个线程访问 , 发现同步监视器被锁定 , 无法访问 .
- 第一个线程访问完毕 , 解锁同步监视器 .
- 第二个线程访问, 发现同步监视器没有锁 , 然后锁定并访问
10.3、加锁的买票案例
使用synchronized 方法去控制对"对象"的访问, 此时对 买票 这个方法进行加锁.
那么从此时对buy()方法添加关键词synchronized 后为 private synchronized void buy() 相当于锁住。
同步方法避免了线程不安全的问题,如数据紊乱的问题避免了。
package com.AL.Multithread;
// 线程不安全,数据紊乱。
// 不安全的买票。 会出现票数为负
public class UnsafeBuyTicket {
public static void main(String[] args){
BuyTicket station = new BuyTicket();
new Thread(station,"可怜的我").start();
new Thread(station,"得意的你").start();
new Thread(station,"可恶的黄牛党").start();
}
}
class BuyTicket implements Runnable{
// 票数
private int ticketNums = 10;
boolean flag = true; // 外部停止方式,标志位
@Override
public void run() {
// 买票
while (flag){
try {
buy();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 买票,这一部分是为了去改变 flag,便于线程停止
//private void buy() throws InterruptedException{
private synchronized void buy() throws InterruptedException{//添加关键词synchronized,相当于锁住,同步方法.避免了线程不安全的问题,如数据紊乱的问题避免了
// 判断是否有票
if (ticketNums <= 0){
flag = false;
return;
}
//模拟延时。 可以放大 问题的发生性
Thread.sleep(100);
//买票
System.out.println(Thread.currentThread().getName()+"拿到了"+ticketNums--);
}
}
结果:
可怜的我拿到了10
可怜的我拿到了9
可怜的我拿到了8
可怜的我拿到了7
可恶的黄牛党拿到了6
可恶的黄牛党拿到了5
得意的你拿到了4
得意的你拿到了3
得意的你拿到了2
得意的你拿到了1
10.4、加锁的银行取钱案例
在取钱的这个例子中, 我们能直接对取钱的 run 方法这里添加关键词后为: public synchronized void run() , 但是结果仍然有问题, 因为在这个run方法中,默认锁住的是 this。
取钱的例子结果表明,我们没有完全锁住。 我们应该去锁住 account这个账户,修改如下:即采用synchronized(Obj)块去解决。锁的对象应该是 增删查改的对象,即不是锁银行,应该是锁账户。
package com.AL.Multithread;
//不安全的取钱
// 两个人去银行取钱, 账户
public class UnsafeBank {
public static void main(String[] args){
//账户
Account account = new Account(100,"结婚基金");
Drawing you=new Drawing(account,50,"你");
Drawing girlFriend=new Drawing(account,100,"女朋友");
you.start();
girlFriend.start();
}
}
//账户
class Account{
int money; //余额
String name; //卡名
public Account(int money, String name){
this.money = money;
this.name = name;
}
}
//银行: 模拟取款
class Drawing extends Thread{
Account account; //账户
int drawingMoney; //取了多少钱
int nowMoney; // 现在卡里还有多少钱
public Drawing(Account account, int drawingMoney, String name){
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
//取钱
@Override
public void run() {
synchronized (account) { //Synchronized(Obj){}同步块,锁住增删查改的对象
//判断有没有钱
if (account.money - drawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + "钱不够,取不了");
return;
}
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
// 卡内余额 = 余额-你取的钱
account.money = account.money - drawingMoney;
// 你手里的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name + "余额为:" + account.money);
System.out.println(this.getName() + "手里的钱:" + nowMoney);
}
}
}
结果:
结婚基金余额为:50
你手里的钱:50
女朋友钱不够,取不了
10.5、集合的案例
JUC并发编程,保护线程安全的一些类。
package com.AL.Multithread;
import java.util.ArrayList;
import java.util.List;
public class SafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 100000; i++) {
new Thread(()->{
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
}
for (int i = 5;i>0;i--){
Thread.sleep(1000);
System.out.println("倒计时"+i);
}
System.out.println(list.size());
}
}
或者我们可以选择能够保证线程安全的List, 即使用 CopyOnWriteArrayList。如下所示:
package com.AL.Multithread;
import java.util.concurrent.CopyOnWriteArrayList;
public class SafeJUCList {
public static void main(String[] args) throws InterruptedException {
//保证线程安全的list , ArrayList
CopyOnWriteArrayList list = new CopyOnWriteArrayList();
for (int i = 0; i < 100000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
for (int i = 5;i>0;i--){
Thread.sleep(1000);
System.out.println("倒计时"+i);
}
System.out.println(list.size());
}
}
上述两种方法都能解决集合List的线程安全问题,结果如下:
倒计时5
倒计时4
倒计时3
倒计时2
倒计时1
100000
11、死锁
死锁
- 多个线程各自占有一些共享资源 , 并且互相等待其他线程占有的资源才能运行 , 而导致两个或者多个线程都在等待对方释放资源 , 都停止执行的情形 。某一个同步块同时拥有 “ 两个以上对象的锁 ” 时 , 就可能会发生 “ 死锁 ” 的问题 .
两个或多个线程都在等待对方释放资源,结果产生了都停止执行的情形,即死锁。
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
死锁避免方法:我们只要将这四个必要条件破坏掉任意一个就可以避免死锁发生。
例子:多个线程去共用同一个资源,多个线程互相拥抱着对方需要的资源,然后形成僵持,造成了死锁。
在灰姑娘或者白雪公主任意一个进程去进行多线程时, 因为它们各自都会紧握着对方需要的那个锁的资源,你在抱着这两个锁的资源时,还想让对方给你想要的,对方并没有得到。
拿了镜子的锁,里面还包含想要口红的锁;但是有了口红的锁,还包含着镜子的锁,这就造成了同一资源被两个进程调用。所以就造成了死锁。
为了避免死锁,白雪公主或者灰姑娘,在已经拥有一个锁的时候,就把另外一个锁放出去,然后等待得到自己想要的锁 资源,就能避免死锁了。
在代码中,注释部分是死锁的案例,即同一资源被两个进程去调用。 将锁修改到外面的代码块, 不让他抱对方的锁,就可以解决问题。
package com.AL.Multithread;
//死锁: 多个线程共用同一个资源。
// 多个线程互相拥抱着对方需要的资源,然后形成僵持
public class DeadLock {
public static void main(String[] args){
Makeup g1 = new Makeup(0,"灰姑娘");
Makeup g2 = new Makeup(1,"白雪公主");
g1.start();
g2.start();
}
}
//口红
class Lipstick{
}
//镜子
class Mirror{
}
class Makeup extends Thread {
//需要的资源只有一份,用static来保证只有一份
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice; //选择
String girlNmae; //使用化妆品的人
Makeup(int choice, String girlNmae) {
this.choice = choice;
this.girlNmae = girlNmae;
}
@Override
public void run() {
//化妆
try {
makeup();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
//此时, 同一个资源被两个进程调用,造成了死锁
private void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) { //设置口红的锁, 同步块
System.out.println(this.girlNmae + "获得口红的锁");
Thread.sleep(1000);
synchronized (mirror) { // 一秒钟后想要获得镜子
System.out.println(this.girlNmae + "获得镜子的锁");
}
}
} else {
synchronized (mirror) { // 获得镜子的锁
System.out.println(this.girlNmae + "获得镜子的锁");
Thread.sleep(1000);
synchronized (lipstick) { // 一秒钟后想要获得口红
System.out.println(this.girlNmae + "获得口红的锁");
}
}
}
}
*/
private void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) { //设置口红的锁, 同步块
System.out.println(this.girlNmae + "获得口红的锁");
Thread.sleep(1000);
}
synchronized (mirror) { // 一秒钟后想要获得镜子
System.out.println(this.girlNmae + "获得镜子的锁");
}
} else {
synchronized (mirror) { // 获得镜子的锁
System.out.println(this.girlNmae + "获得镜子的锁");
Thread.sleep(1000);
}
synchronized (lipstick) { // 一秒钟后想要获得口红
System.out.println(this.girlNmae + "获得口红的锁");
}
}
}
}
结果:
白雪公主获得镜子的锁
灰姑娘获得口红的锁
白雪公主获得口红的锁
灰姑娘获得镜子的锁
12、Lock(锁)
Lock锁
- JDK5.0开始,java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
对于Lock(锁),它的语法如下,特别注意的是显示加锁和释放锁;
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock(); //加锁
try{
//保证线程安全的代码;
}
finally{
lock.unlock(); //解锁
//如果同步代码有异常,要将unlock()写入finally语句块
}
}
synchronized 与 Lock 的对比:
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
synchronized 与 Lock优先使用顺序:
Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)
ReentrantLock类实现了Lock,其中的ReentrantLock称为可重入锁。
下方的例子:买票的案例,注释部分为没有加锁的部分,多线程会发生并发问题,造成数据紊乱。 修改后的代码中,使用了ReentrantLock类,实例化对象为 lock, 然后进行加锁和解锁,保证线程安全性。
package com.AL.Multithread;
import java.util.concurrent.locks.ReentrantLock;
//测试Lock锁
// 使用ReentrantLock类实现了Lock
public class TestLock {
public static void main(String[] args){
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
/**
class TestLock2 implements Runnable{
int ticketNums = 10;
@Override
public void run() {
while (true){
if(ticketNums > 0){
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
}else {
break;
}
}
}
}
*/
class TestLock2 implements Runnable {
int ticketNums = 10;
// 定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock(); // 加锁
if (ticketNums > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
} else {
break;
}
} finally {
//解锁
lock.unlock();
}
}
}
}
结果:
10
9
8
7
6
5
4
3
2
1