JAVA基础——多线程
Posted 我永远信仰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA基础——多线程相关的知识,希望对你有一定的参考价值。
3、线程同步
1、三大不安全案例
1.不安全的买票
每个线程都在自己的内存交互,内存控制不当会造成数据不一致。
当多个线程同时进入临界区的时候,他们看到的都是同样的票数,一个线程买了票后,执行了票数-1,另一个线程在这基础上又执行了一次,就会出现负数。比如当票只剩一张了,a、b同时进入临界区,买了两张票,所以会出现-1。
另一种情况是,他们同时对票数-1,所以买了两张票后,发现票数只减了一次。
//不安全的买票,线程不安全
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket, "皇小明").start();
new Thread(buyTicket, "孙大红").start();
new Thread(buyTicket, "加奈量").start();
}
}
class BuyTicket implements Runnable {
private int ticketNum = 10;
private boolean flag = true;
@Override
public void run() {
while (flag) {
//模拟延时
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void buy() throws InterruptedException {
if (ticketNum <= 0) {
flag = false;
return;
}
Thread.sleep(100);
//买票
System.out.println(Thread.currentThread().getName() + "买到票" + ticketNum--);
}
/*
皇小明买到票10
孙大红买到票8
加奈量买到票9
加奈量买到票7
孙大红买到票6
皇小明买到票6
孙大红买到票5
皇小明买到票5
加奈量买到票5
孙大红买到票4
皇小明买到票2
加奈量买到票3
皇小明买到票1
孙大红买到票0
加奈量买到票1
皇小明买到票-1
*/
}
2.不安全的集合
如果两个线程同时插入到一个地方,会进行覆盖,就达不到1万的数量
public class UnsafeList {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
System.out.println(list.size());
Thread.sleep(3000);
System.out.println(list.size());
}
/*结果
9794
9998
*/
}
3.不安全的取钱
和买票差不多。
2、同步方法
处理多线程问题,(比如上面2.1总结处提到的抢票,同一张票被抢了两次,同一张票只应该被抢一次的问题)多个线程访问同一个对象(并发),并且某些线程还想修改这个对象,这是还我们就需要线程同步。
队列
线程同步其实是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程执行完毕,下一个才能执行。
比如多个人想要上厕所,而厕所只有一个,为了维持秩序,最好的方法是排队。但这也并不能解决问题,如果有人要插队,那么这些人可能会打起来去抢占资源,或者一堆人挤到一个厕所里,这样厕所会爆炸。所以需要一个锁机制。
锁
只有队列并不能解决并发的问题,锁机制能防止代码块被干扰。这一结构确保任何时刻只有一个线程进入临界区,一旦一个线程封锁了锁对象,其他任何线程都无法通过,他们被阻塞,知道第一个线程释放锁对象。
比如每个人(线程)都有一把锁,一个人进入厕所(临界区)之后,将其上锁,外面的人是无法通过锁机制的,直到等到里面的人出来(释放锁)。
队列+锁保证了线程的安全性。
锁带来了安全性,但同时也会损失性能。鱼和熊掌不可兼得,只有一个厕所,不能保证所有人能同时上厕所还要上的舒服。
1.synchronized关键字
- 方法里面需要修改的内容才需要锁,锁的太多反而浪费资源。
将上面不安全案例一 :买票的buy方法加上关键字synchronized线程即变为安全线程(synchronized方法)
private synchronized void buy()
锁的对象是变化的量,需要增删查改的对象
将上面不安全案例二 :增加一个synchronized块锁住list即可。
synchronized (list){
list.add(Thread.currentThread().getName());
}
2、Lock锁对象
- Lock——通过显式定义同步锁对象来实现同步。
- ReentrantLock(可重入锁)类实现了Lock,它拥有与synchronized相同的并发性和内存语义。
一个实例
Lock加锁方式,如果同步代码有异常,需要将unlock()写入到finally里面。确保锁一定会释放,
//两个线程模拟抢票
import java.util.concurrent.locks.ReentrantLock;
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();//一个资源
new Thread(testLock2).start();//线程1
new Thread(testLock2).start();//2
}
}
class TestLock2 implements Runnable {
private int ticket=10; //总票数
ReentrantLock lock = new ReentrantLock();//获得锁对象
@Override
public void run() {
while (true) {
if (ticket < 1) {
break;
}
try {
lock.lock();
try { //有异常的同步代码块,外面需要try、finally释放锁。
//模拟网络延时:放大问题的发生性
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.unlock();
}
System.out.println(ticket--);
}
}
}
总结:
- Lock是显示锁(手动开启和关闭),synchronized是隐式锁,粗话了作用域自动释放。
- Lock只有代码块锁,synchronized又代码块锁和方法锁
- 使用Lock锁,JVM将花费更少的时间来调度进程,性能更好,并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序
- Lock–>同步代码块(方法体中的)–>同步方法
3、死锁
多个线程各自占有一些公共资源,并且互相等待其他线程占有的资源才能运行,而导致多个线程都在等待对方释放资源,都停止执行的情形,称为死锁。
某一个同步块拥有两个以上的对象的锁时,就可能引发死锁问题
模拟死锁:
两个司机一开始分别就拿到了车钥匙和车,都在互相等待对方的资源释放。形成死锁
public class DeadLock {
public static void main(String[] args) {
DriveCar driveCar1 = new DriveCar("老司机",0);
DriveCar driveCar2 = new DriveCar("新手司机",1);
new Thread(driveCar1).start();
new Thread(driveCar2).start();
}
}
//车
class Car {
}
//车钥匙
class CarKeys {
}
//开车的线程,需要集齐才能开车
class DriveCar implements Runnable {
//资源:只有一把钥匙、一辆车
static Car car = new Car();
static CarKeys carKeys = new CarKeys();
int have;//拥有的资源数量
String name;//开车的人
DriveCar(String name, int have) {
this.name = name;
this.have = have;
}
@Override
public void run() {
//开车
try {
driverCar();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void driverCar() throws InterruptedException {
if (have == 0) {
synchronized (car) { //拿到车
System.out.println(this.name + "获得车的使用权");
Thread.sleep(1000);
synchronized (carKeys) {//拿到车钥匙
System.out.println(this.name + "拿到车钥匙");
}
}
} else {
synchronized (carKeys) { //拿到车钥匙
System.out.println(this.name + "拿到车钥匙");
synchronized (car) {//拿到车
System.out.println(this.name + "获得车的使用权");
}
}
}
}
}
解决上面案例的死锁
将开车的代码改为两把锁,破环形成死锁的条件。
private void driverCar() throws InterruptedException {
if (have == 0) {
synchronized (car) { //拿到车
System.out.println(this.name + "获得车的使用权");
Thread.sleep(1000);
}
synchronized (carKeys) {//拿到车钥匙
System.out.println(this.name + "拿到车钥匙");
}
} else {
synchronized (carKeys) { //拿到车钥匙
System.out.println(this.name + "拿到车钥匙");
Thread.sleep(1000);
}
synchronized (car) {//拿到车
System.out.println(this.name + "获得车的使用权");
}
}
}
java编程语言中没有任何东西可以避免或打破这种死锁现象,必须仔细设计程序,以确保不会出现死锁。
4、线程协作(生产者消费者模式)
1.线程通信
- 应用场景∶生产者和消费者问题
- 假设仓库中只能存放一件产品﹐生产者将生产出来的产品放入仓库﹐消费者将仓库中产品取走消费.
- 如果仓库中没有产品,则生产者将产品放入仓库﹐否则停止生产并等待,直到仓库中的产品被消费者取走为止.
- 如果仓库中放有产品﹐则消费者可以将产品取走消费﹐否则停止消费并等待,直到仓库中再次放入产品为止.
2.线程通信-分析
- 这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件.
- 对于生产者,没有生产产品之前,要通知消费者等待.而生产了产品之后﹐又需要马上通知消费者消费
- 对于消费者﹐在消费之后﹐要通知生产者已经结束消费﹐需要生产新的产品以供消费.
- 在生产者消费者问题中,仅有synchronized是不够的
- synchronized可阻止并发更新同一个共享资源,实现了同步
- synchronized不能用来实现不同线程之间的消息传递(通信)
Java提供了几个方法解决线程之间的通信问题
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同﹐会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度 |
**注意:**均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常lllegalMonitorStateException
实例:(管程法)
管程法使用缓冲区
生产者生产10个鸡放到缓冲区和消费者从缓冲区拿鸡
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Producer(container).start();
new Consumer(container).start();
}
}
//生产者
class Producer extends Thread{
SynContainer container;
public Producer(SynContainer container){
this.container = container;
}
//生产
@Override
public void run() {
for (int i = 0; i < 100; 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 = 0; i < 100; i++) {
System.out.println("消费了-->"+container.pop().id+"只鸡");
}
}
}
//产品
class Chicken{
int id; //产品编号
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;
}
}
实例(信号灯法)
信号灯使用标志位
package com.thread.pro_con;
/**
* @Author cyh
* @Date 2021/8/6 22:01
*/
//信号灯法:标志位
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("斗罗大陆:第"+i+"集");
}
}
}
}
//观众观看节目
class Watcher extends Thread{
TV tv;
public Watcher(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++java基础入门-多线程同步浅析-以银行转账为样例