引言
2021年3月5日14:20:28
知识均总结与: Java全栈学习网站. 以及自己的一些心得体会
线程同步
多个线程操作同一个资源
相关概念:
- 并发:同一对象被多个线程同时操作,如上万人同时抢一张票
- 锁机制:
同一进程的多个线程共享同一块存储空间,为了保证数据在方法中被访问时的正确性,在访问时加入了锁机制synchronized
当一个线程对象获得排它锁,独占资源,其他线程必须等待,使用后释放锁即可
线程同步是一种等待机制,多个需要同时访问此对象的线程进入一个对象等待池
形成队列,等待前面线程使用完毕后,下一个线程再使用
锁机制的问题:
- 持有锁的线程会导致其他需要锁的线程挂起
- 加锁,释锁会导致调度延时,引发性能问题
不安全案例:
不安全买票
package work;
//不安全买票
public class Test_7 {
public static void main(String[] args) {
BuyTicket t1 = new BuyTicket();
new Thread(t1,"mg").start();
new Thread(t1,"qh").start();
new Thread(t1,"whs").start();
new Thread(t1,"tlr").start();
}
}
class BuyTicket implements Runnable{
private int ticketNums = 10;
private boolean flag = true;
@Override
public void run() {
// TODO 自动生成的方法存根
//买票
while(flag) {
buy();
}
}
private void buy() {
//判断是否有票
if (ticketNums <=0) {
return;
}
//买票
System.out.println(Thread.currentThread().getName()+"拿到"+ticketNums-- + "票");
}
}
银行取钱
package work;
//不安全的取钱
public class Test_8 {
public static void main(String[] args) {
Account account = new Account(100,"结婚基金");
Drawing you = new Drawing(account, 50,"你");
Drawing girlFriend = new Drawing(account, 100,"girlFriend");
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;
}
public void run() {
//判断有没有钱
if(account.money - drawingMoney <0) {
System.out.println(Thread.currentThread().getName()+"钱不够,取不了" );
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
account.money = account.money - drawingMoney;
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name +":余额为"+account.money);
System.out.println(this.getName()+":手里的钱为"+nowMoney);
}
}
添加东西到同一位置
package work;
import java.util.List;
import java.util.ArrayList;
//线程不安全集合
public class Test_9 {
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();;
}
System.out.println(list.size());
}
}
解决同步问题
通过private关键字来保证数据对象只能被方法访问
因此利用同步方法、同步块——》均是synchronized关键字,来解决同步问题
synchronized方法:
public synchronized void run() {
- 控制对象的访问,每个对象对应一把锁
- 每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程就会阻塞
- 方法一旦执行,就独占锁,直到方法返回才释放锁
- 影响效率
package work;
//不安全买票
public class Test_7 {
public static void main(String[] args) {
BuyTicket t1 = new BuyTicket();
new Thread(t1,"mg").start();
new Thread(t1,"qh").start();
new Thread(t1,"whs").start();
}
}
class BuyTicket implements Runnable{
private int ticketNums = 1000;
private boolean flag = true;
@Override
public void run() {
// TODO 自动生成的方法存根
//买票
while(flag) {
buy();
}
}
//同步方法,锁的是BuyTicket本身
private synchronized void buy() {
//判断是否有票
if (ticketNums <=0) {
return;
}
//买票
System.out.println(Thread.currentThread().getName()+"拿到"+ticketNums-- + "票");
}
}
同步块:
同步块:synchronized(Obj) {}
Obj称为同步监视器:
- Obj可是任何对象,推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,同步方法的同步监视器就是this,就是对象本身,或者class【反射中讲解】
同步监视器执行过程:
3. 第一个线程访问,锁定同步监视器,执行其中代码
4. 第二个线程访问,发现同步监视器被锁定,无法访问
5. 第一个线程访问完毕,解锁同步监监视器
6. 第二个线程访问,发现同步监视器没锁定,执行其中代码
package work;
//不安全的取钱
public class Test_8 {
public static void main(String[] args) {
Account account = new Account(100,"结婚基金");
Drawing you = new Drawing(account, 50,"你");
Drawing girlFriend = new Drawing(account, 100,"girlFriend");
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;
}
public void run() {
synchronized (account) { //同步块
//判断有没有钱
if(account.money - drawingMoney <0) {
System.out.println(Thread.currentThread().getName()+"钱不够,取不了" );
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
account.money = account.money - drawingMoney;
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name +":余额为"+account.money);
System.out.println(this.getName()+":手里的钱为"+nowMoney);
}
}
}
注:锁的对象都是变化的量
补充:JUC实现安全集合
package work;
import java.util.concurrent.CopyOnWriteArrayList;
//测试JUC安全类型的集合
public class Test_11 {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
for(int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
System.out.println(list.size());
}
}
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形——死锁
例子:下面就会产生死锁
package work;
import java.awt.List;
//死锁,多个线程互相持有对方需要的资源,然后相互等待
public class Test_12 {
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 girName; //人名
public Makeup(int choice,String girName) {
this.choice = choice;
this.girName = girName;
}
public void run() {
try {
makeup();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
//化妆,互相持有对方想要的资源,口红与镜子
public void makeup() throws InterruptedException {
if(choice == 0) {
synchronized (lipstick) { //获得口红的锁
System.out.println(this.girName+"获得口红的锁");
Thread.sleep(1000);
synchronized (mirror) {
System.out.print(this.girName+"获得镜子的锁");
}
}
}else {
synchronized (mirror) { //获得镜子的锁
System.out.println(this.girName+"获得镜子的锁");
Thread.sleep(2000);
synchronized (lipstick) {
System.out.print(this.girName+"获得口红的锁");
}
}
}
}
}
产生死锁的四个必要条件:
- 互斥条件,一个资源只能被一个进程使用
- 请求与保持条件:一个进程因请求资源阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得资源,未使用完之前,不能强行剥夺
- 循环等待:若干进程之间形成一种头尾相接的循环等待资源的关系。
破除其中的一个或多个条件,我们就可以避免死锁的发生。
Lock锁
-
JDK5.0开始,Java提供的线程同步机制,通过显示定义同步锁对象实现同步,同步锁使用Lock对象充当。
-
java.utilconcurrent.Lock接口是控制多个线程对资源共享进行访问的工具,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应获得Lock对象。
-
ReentrantLock(可重复锁) 类实现了Lock,它拥有与synchronized相同的并发性与内存语义,比较常用ReentrantLock,可以显示加锁、释放锁
package work;
import java.util.concurrent.locks.ReentrantLock;
//测试Lock锁
public class Test_13 {
public static void main(String[] args) {
TestLock2 t1 = new TestLock2();
new Thread(t1).start();
new Thread(t1).start();
new Thread(t1).start();
}
}
class TestLock2 implements Runnable{
int ticketNums = 10;
//定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
// TODO 自动生成的方法存根
while(true) {
try {
lock.lock(); //加锁
if(ticketNums > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
System.out.println(ticketNums--);
}else {
break;
}
}
finally {
// TODO: handle finally clause
//解锁
lock.unlock();
}
}
}
}
生产者消费者(线程协作)
生产者:生产者将生产出的产品放入仓库,没有产品之前通知消费者等待,有了产品后,通知消费者
消费者:消费后,通知生产者消费结束,需要生产新的产品以供消费。
该问题中:仅使用synchronized是不够的。
Java提供的几个方法解决现存之间的通信
Column 1 | Column 2 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,与sleeo不同,会释放锁 |
wait(long timeout)) | 指定等待毫秒 |
notify() | 唤醒一个处于等待的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级高的先调度 |
注:均为Object类的方法,只能在同步方法或者同步代码块中使用。否则抛出异常
缓冲区:生产者将产品放入缓冲区,消费者从缓冲区取走数据
管程法(利用缓冲区)
package work;
import java.util.concurrent.CountDownLatch;
//测试生参者,消费者模型——>利用缓冲区解决:管程法
public class Test_14 {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Consummer(container).start();
}
}
//生产者
class Productor extends Thread{
SynContainer container;
public Productor(SynContainer container) {
this.container = container;
}
//生产
public void run() {
for(int i = 0; i< 100; i++) {
System.out.println("生产了" + i + "只鸡");
container.push(new Chicken(i));
}
}
}
//消费者
class Consummer extends Thread{
SynContainer container;
public Consummer(SynContainer container) {
this.container = container;
}
//消费
public void run() {
for(int i = 0; i< 100; 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 (Exception e) {
// TODO: handle exception
}
}
//如果没有满,我们就需要丢人产品
chickens[count] = chicken;
count++;
//通知消费者消费
this.notifyAll();
return ;
}
//消费者消费产品
public synchronized Chicken pop() {
//判断是否消费
if(count == 0) {
//等待生产者生产
try {
this.wait();
} catch (Exception e) {
// TODO: handle exception
}
}
//如果可以消费
count--;
Chicken chicken = chickens[count];
//通知生产者,我吃完了
this.notifyAll();
return chicken;
}
}
信号灯法
利用一个标志位来解决消费者问题
package work;
//信号灯法
public class Test_15 {
public static void main(String[] arg) {
TV tv = new TV();
new Player(tv).start();
new Wathcer(tv).start();
}
}
//生产者--》演员
class Player extends Thread{
TV tv = new TV();
public Player(TV tv) {
this.tv = tv;
}
public void run() {
for(int i = 0; i < 20; i++) {
if (i%2 == 0 ) {
this.tv.play("快乐大本营播放着");
}else {
this.tv.play("抖音记录美好生活");
}
}
}
}
//消费者--》观众
class Wathcer extends Thread{
TV tv = new TV();
public Wathcer(TV tv) {
this.tv = tv;
}
public void run() {
for(int i = 0; i < 20; i++) {
this.tv.watch();
}
}
}
// 产品--》节目
class TV{
//演员表演,观众等待
//观众观看,演员等待
String voice; //表演的节目
boolean flag = true; //信号灯法,利用标志位
//表演
public synchronized void play(String voice) {
if(!flag) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
System.out.println("演员表演了");
//通知观众观看
this.notifyAll();
this.voice = voice;
this.flag = !this.flag;
}
//观看
public synchronized void watch() {
if(flag) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
System.out.println("观看完了"+voice);
//通知演员
this.notifyAll();
this.flag = !this.flag;
}
}
线程池
经常创建和消耗线程会消耗大量资源,对性能影响很大
线程池:
1.提前创建好多个线程,放入线程池,使用时直接获取,使用完放回池中
2.提高响应速度,降低资源消耗,便于线程管理
corePoolSize------核心池大小
maximumPoolSize-------最大线程数
keepAliveTime--------线程没有任务时最多保持多长时间后会终止
JDK5.0提供了线程相关API:ExecutorService 和 Executors
- ExecutorService:真正的线程池接口,常见的子类ThreadPoolExecutor
- void execute( Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task);执行任务,有返回值,一般来执行Callable(前面我们用的创建线程的第3种方式)
- void shutdown():关闭连接池
Executors:工具类,线程池的工厂,用于创建并返回不同类型的线程池
package work;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//测试线程池
public class Test_16 {
public static void main(String[] args) {
//创建服务,创建线程池
ExecutorService service = Executors.newFixedThreadPool(10); //参数为线程池大小
//执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//关闭连接池
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
// TODO 自动生成的方法存根
System.out.println(Thread.currentThread().getName());
}
}