多线程三大不安全案例
案例一:
package com.TestThread;
/*
* 不安全的买票
* 线程不安全,有负数
* 因为每个线程在自己的工作内存交互,内存控制不当会造成数据不一致.假如有三个人同时抢最后一张票
* 三个人买的时候看到的都是1,都将1拷贝到自己的内存了,第一个人买了,就变成0了,第二个人再买就变成-1了
* 模拟延时是为了放大问题的发生性!
* */
public class UnsafeBuyticket {
public static void main(String[] args) {
Buyticket station = new Buyticket(); //station:火车站
new Thread(station,"A").start();
new Thread(station,"B").start();
new Thread(station,"C").start();
}
}
class Buyticket implements Runnable{
boolean flag = true; //外部停止方式
private int ticketNums = 10; //票
@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--);
}
}
运行结果:
C拿到10
B拿到8
A拿到9
B拿到7
A拿到5
C拿到6
C拿到4
A拿到2
B拿到3
B拿到1
A拿到-1
C拿到0
案例二:
package com.TestThread;
/*不安全取钱
两个人去银行取钱,账户有100万,你们两看见都是100万都可以取,
但是一操作后就会出现负数,造成不安全的取钱。
线程的内存都是各自的,互不影响,都是从原来的地方拷贝过去的。
*/
public class UnsafeBank {
public static void main(String[] args) {
//账户
Account account = new Account(100, "结婚基金");
Drawing you = new Drawing(account, 50, "You");
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;
}
//取钱
@Override
public void run() {
//判断有没有钱
if (account.money - drawingMoney < 0){
System.out.println(Thread.currentThread().getName()+"余额不足,去不了");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException 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
girlFriend手里的钱100
结婚基金余额为:-50
You手里的钱50
案例三:
package com.TestThread;
import java.util.ArrayList;
import java.util.List;
//线程不安全的集合:
//因为两个线程,同一瞬间操作了同一个位置,把两个数组添加到了同一个位置,于是就把他覆盖掉了,元素就会少了
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
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());
}
}
运行结果;
9998
线程同步_Synchronized
解决线程同步安全问题,Java提供了三种方式
- 同步代码块
- 同步方法
- Java juc包(锁机制)
1.同步代码块
- 为了解决并发操作可能造成的异常,java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。其语法如下:
synchronized(obj){
//同步代码块
//obj可以是任意共享的资源(对象),同步代码块同步的是对象
}
- 其中obj就是同步监视器,它的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。虽然java程序允许使用任何对象作为同步监视器,但 是同步监视器的目的就是为了阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。
2.同步方法
修饰符 synchronized 返回值 方法名(){
//方法体}
针对上面的案例一进行线程同步操作,程序使用synchronized将buy()方法里的方法体修改成同步代码块即可;
package com.TestThread;
//线程同步买票
public class UnsafeBuyticket {
public static void main(String[] args) {
Buyticket station = new Buyticket(); //station:火车站
new Thread(station,"A").start();
new Thread(station,"B").start();
new Thread(station,"C").start();
}
}
class Buyticket implements Runnable{
boolean flag = true; //外部停止方式
private int ticketNums = 10; //票
@Override
public void run() {
//买票
while (flag){
try {
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void synchronized buy() throws InterruptedException {
//判断是否有票
if (ticketNums <= 0){
flag = false;
return;
}
//模拟延时
Thread.sleep(100);
//买票
System.out.println(Thread.currentThread().getName()+"拿到"+ticketNums--);
}
}
3.测试JUC安全类型的集合
package com.TestThread;
//测试JUC安全类型的集合
import java.util.concurrent.CopyOnWriteArrayList;
public class JucDemo {
public static void main(String[] args) {
CopyOnWriteArrayList<Object> list = new CopyOnWriteArrayList<>();
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());
}
}
死锁
死锁
多个线程各自占用一些共享资源,并且互相等待其他线程占用的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有"两个以上的对象"时,就可能会发生死锁问题.
产生死锁的可能原因有:
-
系统资源不足
-
资源分配不当
-
进程/线程运行推进的顺序不合适
java 死锁产生的四个必要条件: -
互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
-
不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
-
请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
-
循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
- 当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失.
- 解决死锁问题的方法是:一种是用synchronized,一种是用Lock显式锁实现。
死锁的处理
-
预防死锁。
这是一种较简单和直观的事先预防的方法。方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。预防死锁是一种较易实现的方法,已被广泛使用。但是由于所施加的限制条件往往太严格,可能会导致系统资源利用率和系统吞吐量降低。 -
避免死锁。
该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。 -
检测死锁。
这种方法并不须事先采取任何限制性措施,也不必检查系统是否已经进入不安全区,此方法允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源,然后采取适当措施,从系统中将已发生的死锁清除掉。 -
解除死锁。
这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。死锁的检测和解除措施,有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。
Lock锁
- 从JDK 5.0开始.Java提供了更强大的线程同步机制-----通过显示定义同步锁对象实现同步;同步锁使用Lock对象充当.
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具;锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应获得Lock对象;
- ReentrantLock 类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁;
代码实例
package com.TestThread;
//测试Lock锁
import java.util.concurrent.locks.ReentrantLock;
public class JockDemo {
public static void main(String[] args) {
JockDemo2 jockDemo2 = new JockDemo2();
new Thread(jockDemo2).start();
new Thread(jockDemo2).start();
new Thread(jockDemo2).start();
}
}
class JockDemo2 implements Runnable{
int ticketNums = 10;
//定义锁
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(); //解锁
}
}
}
}
synchronized与Lock的对比
- Lock是显式锁(手动开启和关闭,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放;
- Lock职业代码块锁,synchronized有代码块锁和方法锁;
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好.并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:
. Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
生产者消费者问题
思路
生产者:生产数据;
消费者:消费数据。
消费者在没有数据可供消费的情况下,不能消费;
生产者在原数据没有被消费掉的情况下,不能生产新数据。
假设,数据空间只有一个。
实际上,如果实现了正确的生产和消费,则,两个线程应该是严格的交替执行。
synchronized关键字若用在代码中,形成一个同步块,且,必须要执行锁:
synchronized (锁对象) {
同步块
}
同步块使得锁对象称为thread monitor
生产者消费者模型_管程法
package com.hello;
//测试生产者消费者,---》利用缓存区,和管程
public class ProducerConsumer {
public static void main(String[] args) {
SynContainer container=new SynContainer();
new Producer(container).start();
new Consumer(container).start();
}
}
//生产者
class Producer extends Thread{
SynContainer synContainer;
public Producer(SynContainer synContainer) {
this.synContainer = synContainer;
}
//生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synContainer.push(new Chicken(i));
System.out.println("生产了第"+i+"只鸡");
}
}
}
//消费者
class Consumer extends Thread{
SynContainer synContainer;
public Consumer(SynContainer synContainer) {
this.synContainer = synContainer;
}
//消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了第"+synContainer.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){
//如果容器满了
while(count==10){
//等待,通知消费者消费
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//没满丢入商品
chickens[count++]=chicken;
//通知消费者消费
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop(){
//判断是否消费
while (count==0){
//等待生产者生产
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//可以消费
count--;
Chicken chicken=chickens[count];
//通知生产者快生产
this.notifyAll();
return chicken;
}
}