文章目录
-
目录
线程同步
多个线程操作同一个资源
并发:同一个对象被多个线程同时操作
什么是线程同步
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。
例如,现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,每个人都想吃饭,最天然的解决办法就是,排队,一个个来
线程同步使用环境及机制
处理多线程问题,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下个线程再使用。
为什么要线程同步
多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入的数据会被多次覆盖,所以就要使用线程同步
例如:我们去银行存钱,那肯定是我们银行卡里原本的钱加上要存入的钱。但是在你存钱的同时你的朋友在给你的银行卡转钱,这是两个线程,这两个线程同时拿到了银行卡的本金,那么这两个线程最后都会返回一个总金额,那这两个总金额都是不正确的,只有这两次交易有一个先后顺序才行,这就是线程同步的一个原因。
队列和锁
由于同一进程的多个线程共享同一个存储空间,在带来方便的同时,也带来了访问冲突问题,为保证数据在方法中被访问同时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排他锁,独占资源,其他的线程就必须等待,使用后释放锁即可,存在以下问题:
-
一个线程持有锁会导致其他所有需要次锁的线程挂起
-
在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
-
如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
三大不安全 案例
------不安全买票
package Synchronized;
//不安全的买票
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket ticket = new BuyTicket();
new Thread(ticket,"蔓哥").start();
new Thread(ticket,"涛弟").start();
new Thread(ticket,"思思姐").start();
}
}
class BuyTicket implements Runnable{
// 票
private int ticketNums = 10;
boolean flag = true;//外部停止方式
@Override
public void run() {
// 买票
while(flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void buy() throws InterruptedException {
// 判断是否有票
if (ticketNums<= 0){
flag = false ;
return;
}
// 模拟延时
Thread.sleep(1000);
// 买票
System.out.println(Thread.currentThread().getName()+"买到第"+ticketNums--+"票");
}
}
运行结果
蔓哥买到9票
涛弟买到9票
思思姐买到10票
蔓哥买到8票
思思姐买到7票
涛弟买到6票
思思姐买到4票
蔓哥买到5票
涛弟买到5票
涛弟买到3票
蔓哥买到3票
思思姐买到2票
涛弟买到0票
思思姐买到1票
蔓哥买到1票
------不安全取钱
package Synchronized;
//不安全取钱
//两个人去银行取钱,账户
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,50,"蔓哥");
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 drawingMney;//取了多少钱
int nowMoney;//现在手里有多少钱
public Drawing(Account account,int drawingMney,String name){
super(name);
this.account=account;
this.drawingMney=drawingMney;
}
//取钱
@Override
public void run() {
// 判断是否有钱
if (account.money-drawingMney<=0){
System.out.println(Thread.currentThread().getName()+"卡里钱不够,取不到");
return;
}
// sleep可以放大问题的发生性
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卡内余额 = 余额 - 你取的钱
account.money = account.money - drawingMney;
// 你手里的钱
nowMoney = nowMoney + drawingMney ;
System.out.println(account.name+ "余额:"+account.money);
// 这里 Thread.currentThread().getName() = this.getName()
System.out.println(this.getName()+"手里的钱"+nowMoney);
}
}
运行结果
结婚基金余额:0
结婚基金余额:50
涛弟手里的钱50
蔓哥手里的钱50
------线程不安全的集合
package Synchronized;
import java.util.ArrayList;
import java.util.List;
//线程不安全的集合
public class UnsafeList {
public static void main(String[] args) {
List<String> list= new ArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
运行结果:
9997
同步方法
由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块。
同步方法:
public synchronized void method(int args){}
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法申明为synchronization将会影响效率
补充:方法里面需要修改的内容才需要锁,锁的太多,会浪费资源
同步块
-
同步块:synchronized(Obj){}
-
Obj称之为同步监视器
-
Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
-
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class
-
-
同步监视器的执行过程:
-
第一个线程访问,锁定同步监视器,执行其中代码
-
第二个线程访问,发现同步监视器被锁定,无法访问
-
第一个线程访问完毕,解锁同步监视器
-
第二个线程访问,发现同步监视器没有锁,然后锁定并访问
-
Synchronized使用场景
Synchronized是一个同步关键字,在某些多线程场景下,如果不进行同步会导致数据不安全,而Synchronized关键字就是用代码同步。什么情况下会数据不安全呢,要满足两个条件:
-
数据共享(临界资源)
-
多线程同时访问并改变该数据
例如:
package Synchronized;
public class Accountsyn implements Runnable{
// 共享资源(临界资源)
static int i = 0;
public synchronized void increase(){
i++;
}
@Override
public void run() {
for (int i1 = 0; i1 < 10000; i1++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
Accountsyn accountsyn = new Accountsyn();
Thread t1 = new Thread(accountsyn);
Thread t2 = new Thread(accountsyn);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
运行结果:
20000
但是如果increase是synchronization被删除,那么很可能输出的结果就会小于20000,这是因为多个线程同时访问临界资源i,如果一个线程A对i=88的自增到89没有被B线程读取到,线程B认为i仍然是88,那么线程B对i的自增结果还是89,那么这里就会出现问题
synchronization锁的3种使用形式(使用场景)
-
synchronization修饰普通同步方法:锁对象当前实例对象
-
synchronization修饰静态同步方法:锁对象是当前的类Class对象
-
synchronization修饰同步代码块:锁对象是synchronization后面括号里配置的对象,这里可以是某个对象(xlock),也可以是某个类(Xlock.class)
注意:
使用synchronized修饰非静态方法或者使用synchronized修饰代码块时制定的为实例对象时,同一个类的不同对象拥有自己的锁,因此不会相互阻塞。
使用synchronized修饰类和对象时,由于类对象和实例对象分别拥有自己的监视器锁,因此不会相互阻塞
使用使用synchronized修饰实例对象时,如果一个线程正在访问实例对象的一个synchronized方法时,其它线程不仅不能访问该synchronized方法,该对象的其它synchronized方法也不能访问,因为一个对象只有一个监视器锁对象,但是其它线程可以访问该对象的非synchronized方法。
线程A访问实例对象的非static synchronized方法时,线程B也可以同时访问实例对象的static synchronized方法,因为前者获取的是实例对象的监视器锁,而后者获取的是类对象的监视器锁,两者不存在互斥关系
三大不安全案例(改)
------不安全买票
package Synchronized;
//不安全的买票
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket ticket = new BuyTicket();
new Thread(ticket,"蔓哥").start();
new Thread(ticket,"涛弟").start();
new Thread(ticket,"思思姐").start();
}
}
class BuyTicket implements Runnable{
// 票
private int ticketNums = 10;
boolean flag = true;//外部停止方式
@Override
public void run() {
// 买票
while(flag){
try {
// 模拟延时
Thread.sleep(1000);//注意sleep不会释放锁,所以不能放在buy()方法中
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// synchronized同步方法,锁的是this
private synchronized void buy() throws InterruptedException {
// 判断是否有票
if (ticketNums<= 0){
flag = false ;
return;
}
// 买票
System.out.println(Thread.currentThread().getName()+"买到第"+ticketNums--+"票");
}
}
运行结果:
蔓哥买到第10票
思思姐买到第9票
涛弟买到第8票
蔓哥买到第7票
涛弟买到第6票
思思姐买到第5票
涛弟买到第4票
蔓哥买到第3票
思思姐买到第2票
蔓哥买到第1票
------不安全取钱
package Synchronized;
//不安全取钱
//两个人去银行取钱,账户
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 drawingMney;//取了多少钱
int nowMoney;//现在手里有多少钱
public Drawing(Account account,int drawingMney,String name){
super(name);
this.account=account;
this.drawingMney=drawingMney;
}
//取钱
@Override
public void run() {
// 锁的对象就是变化的量,需要增删改的对象
synchronized(account){
// 判断是否有钱
if (account.money-drawingMney<0){
System.out.println(Thread.currentThread().getName()+"卡里钱不够,取不到"+",卡里余额:"+account.money);
return;
}
// sleep可以放大问题的发生性
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卡内余额 = 余额 - 你取的钱
account.money = account.money - drawingMney;
// 你手里的钱
nowMoney = nowMoney + drawingMney ;
System.out.println(account.name+ "余额:"+account.money);
// 这里 Thread.currentThread().getName() = this.getName()
System.out.println(this.getName()+"手里的钱"+nowMoney);
}
}
}
运行结果:
结婚基金余额:50
涛弟手里的钱50
蔓哥卡里钱不够,取不到,卡里余额:50
------线程不安全的集合
package Synchronized;
import java.util.ArrayList;
import java.util.List;
//线程不安全的集合
public class UnsafeList {
public static void main(String[] args) {
List<String> list= new ArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
运行结果
10000
死锁
-
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有“两个以上对象的锁”时,就可能发生“死锁”的问题
避免死锁方法
-
互斥条件:一个资源每次只能被一个进程使用
-
请求与保持条件:一个进程因请求资源而阻塞时,对以获得的资源保持不放
-
不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
-
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以便面死锁的发生
Lock锁
-
从JDK5.0开始,Java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
-
java.util.concurred.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语句块
}
}
}
代码示例
package thread;
import java.util.concurrent.locks.ReentrantLock;
//测试Lock锁
public class TestLock {
public static void main(String[] args) {
TestLock2 lock2 = new TestLock2();
new Thread(lock2,"a").start();
new Thread(lock2,"b").start();
new Thread(lock2,"c").start();
}
}
class TestLock2 implements Runnable{
int nums = 10 ;
// 定义Lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
// 加锁
lock.lock();
if (nums> 0 ){
System.out.println(Thread.currentThread().getName()+nums--);
}
} finally {
//解锁
lock.unlock();
}
}
}
}
运行结果:
c10
b9
a8
a7
b6
c5
c4
b3
a2
b1
synchronization与Lock对比
-
Lock是显示锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
-
Lock只有代码块,synchronized有代码块和方法锁
-
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
-
优先使用顺序:
-
Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)
-
线程协作
线程通信
-
应用场景:生产者和消费者问题
-
假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费
-
如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
-
如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止
-
线程通信---分析
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
-
对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费
-
对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
-
在生产者消费者问题中,仅有synchronized是不够的
-
synchronization可阻止并发更新同一个共享资源,实现同步
-
synchronization不能用来实现不同线程之间的消息传递(通信)
-
Java提供了几个方法解决线程之间的通信问题
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
botifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度 |
注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IIIegalMonitorStateException
解决方式
------解决方式1
并发协作模型“生产者/消费者模式”------》管程法
-
生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
-
消费者:负责处理数据的模块
-
缓冲区:消费者不能直接使用生产者的数据,它们之间有个“缓冲区”
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
实例:
package thread;
//测试:生产者消费者模型---》利用缓冲区解决:管程法
//生产者,消费者,产品,缓冲区
public class TestPc {
public static void main(String[] args) {
// 缓冲区
SynContainer synContainer = new SynContainer();
// 生产者
new Productor(synContainer).start();
// 消费者
new Consumer(synContainer).start();
}
}
//生产者
class Productor extends Thread{
SynContainer container;
public Productor(SynContainer container){
this.container= container;
}
// 生产
@Override
public void run() {
for (int i = 1; i < 12; i++) {
container.push(new Chicken(i));
System.out.println("生产了"+i+"鸡");
}
}
}
//消费者
class Consumer extends Thread{
SynContainer container;
public Consumer(SynContainer container){
this.container= container;
}
// 消费
@Override
public void run() {
for (int i = 1; i < 12; i++) {
System.out.println("消费了"+container.pop().id+"只鸡");
}
}
}
//产品
class Chicken{
int id ;//产品编号
public Chicken(int id) {
this.id = id;
}
}
//缓冲区
class SynContainer{
// 需要一个容器大小
Chicken[] chickens = new Chicken[10];
// 容器计数器
int count = 0;
// 生产者放入产品
public synchronized void push(Chicken chicken){
// 如果容器满了,就需要等待消费者消费
if (count==chickens.length) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没有,我们就需要丢入产品
chickens[count] = chicken;
count++;
// 可以通知消费者消费了
this.notifyAll();
}
// 消费者消费产品
public synchronized Chicken pop(){
// 判断能否消费
if (count==0){
// 等待生产者生产,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果可以消费
count--;
Chicken chicken = chickens[count];
// 吃完了,通知生产者生产
this.notifyAll();
return chicken;
}
}
------解决方式2
并发协作模型“生产者/消费者模式”------》信号灯法
package thread;
//测试:生产者消费者问题----》信号灯法:标志位解决
public class TestPc2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
//生产者------》演员
class Player extends Thread {
TV tv;
public Player(TV tv){
this.tv =tv ;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i%2==0){
this.tv.play("假面骑士利维斯");
}else{
this.tv.play("奥特曼德凯张凤霞");
}
}
}
}
//消费者------>观众
class Watcher extends Thread{
TV tv;
public Watcher(TV tv){
this.tv =tv ;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
//产品------>节目
class TV{
// 演员表演,观众等待 T ,观众观看,演员等待 F
String voice;//表演的节目
boolean flag = true;
// 表演
public synchronized void play(String voice){
if (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了:"+voice);
// 通知观众观看
this.voice = voice;
this.notifyAll();
this.flag=!flag;
}
// 观看
public synchronized void watch (){
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了:"+voice);
// 通知演员表演
this.notifyAll();
this.flag=!flag;
}
}
运行结果
演员表演了:假面骑士利维斯
观看了:假面骑士利维斯
演员表演了:奥特曼德凯张凤霞
观看了:奥特曼德凯张凤霞
使用线程池
-
背景:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大
-
思路:提前创建好多个线程,放入线程池,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
-
好处:
-
提高 响应速度(减少了创建新线程的时间)
-
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
-
便于线程管理(...)
-
线程池的核心参数:
参数 | 作用 |
---|---|
corePoolSize | 核心线程数量,线程池中应该常驻的线程数量 |
maximumPoolSize | 线程池允许的最大线程数,非核心线程在超时之后会被清除 |
workQueue | 阻塞队列,存储等待执行的任务 |
keepAliveTime | 线程没有任务时最多保持多长时间后会终止 |
unit | 时间单位 |
threadFactory | 线程工厂,用来创建线程 |
rejectHandler | 当拒绝任务提交时的策略(抛出异常、用调用者所在的线程执行任务、丢弃队列中第一个任务执行当前任务、直接丢弃任务) |
-
JDK5.0起提供了线程池相关API:ExecutorService和Executors
-
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
-
void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
-
<T>Future<T>submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable
-
void shutdown():关闭连接池
-
-
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
线程池实例:
package thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//测试线程池
public class TestPool {
public static void main(String[] args) {
// 1.创建服务,创建线程池
// newFixedThreadPool 参数为:线程池大小
ExecutorService service =Executors.newFixedThreadPool(10);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
// 2.关闭连接
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
运行结果
pool-1-thread-1
pool-1-thread-3
pool-1-thread-2