JAVA基础-多线程-下

发布于 2024-07-29  5761 次阅读


线程同步机制

当多个线程需要同时访问和修改同一个对象时,需要创建一个线程池,形成等待队列,逐个依次访问,来避免并发问题

在访问时加入sychronize(锁机制),可以使一个线程访问此对象时获得排他锁,拒绝其他线程对此对象的访问。

线程不安全

下为一个线程不安全的例子

public class testUnsafe {
    public static void main(String[] args) throws InterruptedException {
        you u = new you();
        u.HP = 100;// 初始生命值

        attack atk = new attack(u,"ctf");//来自ctf的攻击
        Thread t = new Thread(atk);
        t.start();

        attack atk1 = new attack(u,"awd");//来自awd的攻击
        Thread t1 = new Thread(atk1);
        t1.start();

    }
}

class you {
     public int HP;//玩家生命值

}
class attack implements Runnable {
    you u;
    String attacker;
    public attack(you u,String atk){ //设置被攻击者和攻击者
        this.u = u;
        this.attacker = atk;
    }

    @Override
    public void run() {

        if(u.HP <= 60){
            System.out.println("You die!");
            System.exit(0);
        }

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }//模拟延迟来放大问题
            System.out.printf("%s攻击了你。\n",attacker);
            u.HP-=60;
            System.out.printf("还剩%d点HP\n",u.HP);

    }
}

alt text
在1s的延迟结束后,两个线程同时访问对象,访问到的都是HP为100的you对象,因此绕过了判断死亡的限制,线程不安全。

sychronize

sychronize:同步锁,在直接修饰方法的时候默认锁的是this,在修饰代码块时,锁括号内的对象:sychronize(obj){code}
在锁定对象时:被修饰方法或代码块内的所有代码线程都对锁定对象,都有队列访问机制,即一个线程访问对象时对象被上锁,被sychronize修饰的线程拿到此锁并访问,其他线程不得同时访问。

上面代码加上同步锁:

public class testUnsafe {
    public static void main(String[] args) throws InterruptedException {
        you u = new you();
        u.HP = 100;// 初始生命值

        attack atk = new attack(u,"ctf");//来自ctf的攻击
        Thread t = new Thread(atk);
        t.start();

        attack atk1 = new attack(u,"awd");//来自awd的攻击
        Thread t1 = new Thread(atk1);
        t1.start();

    }
}

class you {
     public int HP;//玩家生命值

}
class attack implements Runnable {

    you u;
    String attacker;
    public attack(you u,String atk){ //设置被攻击者和攻击者
        this.u = u;
        this.attacker = atk;
    }

    @Override
    public  void run() {
        synchronized (u){ //为对象u上锁
            if(u.HP <= 60){
                System.out.println("You die!");
                System.exit(0);
            }

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }//模拟延迟来放大问题
            System.out.printf("%s攻击了你。\n",attacker);
            u.HP-=60;
            System.out.printf("还剩%d点HP\n",u.HP);

        }

    }
}

alt text

线程死锁

当两个进程互相等待对方释放自己需要的资源,而自己掌握着对方需要的资源锁时,会导致永远互相等待,即进程死锁
例子:

public class DeadLock {
    public static void main(String a[]){
        Thread t1 = new Thread(new change(0,"小明"));
        Thread t2 = new Thread(new change(1,"小红"));

        t1.start();
        t2.start();
    }
}
class incar{
    //在车里
}
class outcar{
    //在车外
}
class change implements Runnable{
    static final incar incar = new incar();
    static final outcar outcar = new outcar();
    int choice ;
    String name;
    public change(int ch,String na){
        this.choice = ch;
        this.name = na;
    }
    public void changepos(){
        if(choice == 1){
            synchronized (incar){
                System.out.printf("现在%s在车上,想下车\n",name);
                synchronized (outcar){
                    System.out.printf("现在%s成功下车\n",name);
                }
            }
        }
        else {
            synchronized (outcar){
                System.out.printf("现在%s在车下,想上车\n",name);
                synchronized (incar){
                    System.out.printf("现在%s成功上车\n",name);
                }
            }
        }

    }
    @Override
    public void run() {
        changepos();
    }
}

alt text
解释:如果只有上车了车里的人才能下车,下车了车外的人才能上车,那么小明永远在等待小红下车,小红永远在等待小明上车,程序卡死

解决方案:不要把对方的锁放到自己的锁块内:

    synchronized (incar){
        System.out.printf("现在%s在车上,想下车\n",name);
        }
    synchronized (outcar){
        System.out.printf("现在%s成功下车\n",name);
        }

Lock:显式定义同步锁

ReentrantLock对象被创建在需要锁定的对象的类中。lock方法锁定对象,unlock解锁。

import java.util.concurrent.locks.ReentrantLock;

public class testUnsafe {
    public static void main(String[] args) throws InterruptedException {
        you u = new you();
        u.HP = 100;// 初始生命值

        attack atk = new attack(u,"ctf");//来自ctf的攻击
        Thread t = new Thread(atk);
        t.start();

        attack atk1 = new attack(u,"awd");//来自awd的攻击
        Thread t1 = new Thread(atk1);
        t1.start();

    }
}

class you {
    public final ReentrantLock lock = new ReentrantLock(); //创建锁
    public int HP;//玩家生命值

}
class attack implements Runnable {

    you u;
    String attacker;
    public attack(you u,String atk){ //设置被攻击者和攻击者
        this.u = u;
        this.attacker = atk;
    }

    @Override
    public  void run() {

        u.lock.lock(); //上锁
        if(u.HP <= 60){
            System.out.println("You die!");
            System.exit(0);
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }//模拟延迟来放大问题
        System.out.printf("%s攻击了你。\n",attacker);
        u.HP-=60;
        System.out.printf("还剩%d点HP\n",u.HP);
        u.lock.unlock(); //解锁

    }
}

线程协作:生产者消费者问题

方法名 作用
wait() 此线程进入等待,直到被其他线程唤醒,与sleep不同,wait()会释放线程锁
wait(long timeout) 线程等待timeout毫秒
notify() 唤醒一个处于等待状态的线程
notifyAll() 唤醒此对象创建的所有线程

以上都是Object类的方法,都只能在同步方法或同步代码块中使用。

生产者消费者问题

生产者线程生产商品,消费者线程消费购买商品。
生产者在生产完商品后要通知消费者购买商品,消费者购买完商品后要通知生产者生产商品。

生产者:负责生成数据的模块
消费者:负责处理数据的模块

1.管程法

生产者将生产好的数据放入缓冲区,消费者负责从缓冲区拿出数据

public class testSynC {
    public static void main(String arg[]){
        SynContainer con = new SynContainer();
        Thread pro = new Thread(new Productor(con));
        Thread cons = new Thread(new Consumer(con));

        pro.start();
        cons.start();
    }
}
class Productor implements Runnable {

    SynContainer con;
    public Productor(SynContainer con){
        this.con = con;
    }
    @Override
    public void run() {
        for(int i = 0 ; i<1000 ; i++){
            System.out.printf("生产了第%d只鸡\n",i);
            try {
                con.push(new chicken(i)); //生产
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

class Consumer implements Runnable{
    SynContainer con;
    public Consumer(SynContainer con){
        this.con = con;
    }
    @Override
    public void run() {
        for(int i = 0 ; i<1000 ; i++){
            try {
                int chickenid = con.pop().id;
                System.out.printf("消费了第%d只鸡 %d\n",chickenid,i); //消费
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }
}}

class chicken{
    int id;
    public chicken(int id){
        this.id = id;
    }

}

class SynContainer{

    int c = 0;
    private chicken[] chickens = new chicken[10];
    public synchronized void push(chicken chicken) throws InterruptedException {

        if(c == chickens.length){

            this.wait(); //如果缓冲区满,则等待消费者消费

        }
        chickens[c++] = chicken;
        this.notifyAll(); //生产出一只鸡,通知消费者消费

    }
    public synchronized chicken pop() throws InterruptedException {

        if(c == 0 ){

            this.wait(); //如果缓冲区没有鸡,等待生产者生产
        }

        chicken chik = chickens[--c]; //消费一只鸡,通知生产者生产
        this.notifyAll(); 
        return chik;

    }
    }

上述代码中,生产者与消费者线程都在不断地进行生产和消费,生产者会在缓冲区满的时候等待消费者消费,消费者会在缓冲区为0时等待生产者生产
alt text

2.信号灯法

public class testFlag {
    public static void main(String arg[]){
        flagHandler con = new flagHandler();
        Thread pro = new Thread(new Kitchen(con));
        Thread cons = new Thread(new Eater(con));

        pro.start();
        cons.start();
    }
}
class Kitchen implements Runnable {

    flagHandler con;
    public Kitchen(flagHandler con){
        this.con = con;
    }
    @Override
    public void run() {
        for(int i = 0 ; i<1000 ; i++){
            try {
                con.push();
                System.out.printf("生产了第%d只鱼\n",i); //消费
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }
    }
}

class Eater implements Runnable{
    flagHandler con;
    public Eater(flagHandler con){
        this.con = con;
    }
    @Override
    public void run() {
        for(int i = 0 ; i<1000 ; i++){
            try {
                con.pop();
                System.out.printf("消费了第%d只鱼\n",i); //消费
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }
    }}

class flagHandler {
    boolean flag = false;//ture:鱼做好了,等顾客吃 ; false:鱼没做好,等厨师做

    public synchronized void push() throws InterruptedException {
        if(flag){
            wait();
        }
        flag=!flag;
        this.notifyAll();

    }
    public synchronized void pop() throws InterruptedException {
        if(!flag){
            wait();
        }
        flag=!flag;
        this.notifyAll();

    }
}

较为简单,用flag区分哪个进程应该等待,哪个进程应该执行
alt text

线程池

池化思想:减少资源消耗提高运行效率
Callable来实现:
将管程法的main函数改为:

    public static void main(String arg[]){
        SynContainer con = new SynContainer();
        ExecutorService ser = Executors.newFixedThreadPool(2); //创建容量为2个线程的线程池

        Future<Boolean> s1 = ser.submit(new Consumer(con)); //提交线程对象到池
        Future<Boolean> s2 = ser.submit(new Productor(con));

        ser.shutdown(); //关闭线程池

    }

实现线程池。

我的疑惑

在缓冲区实现线程交互的例子中,push的逻辑为:

        if(c == chickens.length){

            this.wait(); //如果缓冲区满,则等待消费者消费

        }
        chickens[c++] = chicken;
        this.notifyAll(); //生产出一只鸡,通知消费者消费

为什么改成如下代码时:

        if(c < chickens.length){

            chickens[c++] = chicken;
            this.notifyAll();
        }
        else {
            this.wait();
        }

会出现生产者线程已经结束(即生产者已经全部生产完毕)而消费者线程仍然在运行的情况呢?

A web ctfer from 0RAYS
最后更新于 2024-08-24