Java基础——多线程(二)

线程安全问题

由于每个线程在运行时随时都可能被其他线程抢夺执行权,可能在未执行完完整代码功能时就被挤占掉,因此会造成线程安全问题

同步代码块(内置锁)

把操作共享数据的代码锁起来,防止一个线程在运行时被其它线程挤占掉

特点:

1、锁默认打开,有一个线程进去了,锁自动关闭

2、里面的代码全部执行完毕,线程出来,锁自动打开

操作:

synchronized (这里写锁对象,可以是任意对象,但必须是唯一的) {
    //要锁住的代码块
}

写同步代码块的四步:

1、循环

2、同步代码块

3、判断共享数据是否到了末位(到了末尾)

4、判断共享数据是否到了末位(没有到末尾,执行核心逻辑)

锁对象

锁对象必须是唯一的,如果锁对象不唯一,表示不同线程执行时看的是不同的锁,锁也就失去了意义

一般锁对象的两种写法:

1、类名.class 表示当前类的字节码文件对象(一定是唯一的)

2、public static Object lock = new Object();

同步方法

把synchronized关键字加到方法上

修饰符 synchronized 返回值类型 方法名 (方法参数) {}

特点:

1、同步方法是锁住方法里面所有的代码

2、锁对象不能自己指定:如果方法是非静态,则用this

                                         如果方法是静态的,则用当前类的字节码文件对象

Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁。为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个锁对象Lock


Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作Lock中提供了获得锁和释放锁的方法
void lock():获得锁
void unlock():释放锁
通过以上方法手动上锁、手动释放锁

Lock是接口不能直接实例化,这里采用它的实现ReentrantLock来实例化ReentrantLock的构造方法
ReentrantLock():创建一个ReentrantLock的实例

要求:

1、创建Lock锁对象时,前面加static修饰,表示所有对象共用一把锁

2、unlock方法释放锁,一般写在finally中,确保一定会被执行

利用多线程模拟多个窗口售卖电影票练习代码

public class MyThread extends Thread {
    static Lock lock = new ReentrantLock();//锁对象
    static int ticket = 0;//static修饰表示这个类的所有对象,都共享ticket数据
    //第二种方式实现多线程时不需要加static,测试类中MyRunable只创建一次,所以不需要加static

    @Override
    public void run() {
        while (true) {
            //同步代码块
            //synchronized (MyThread.class) {//将代码块锁起来,如果锁对象是this则锁失效,因为锁对象不一致,this表示运行中的当前线程
            lock.lock();//加锁
            try {
                if (ticket == 100) {
                    break;//TODO 这里break直接跳到while循环外面,没有释放锁,所以unlock要写在finally中
                } else {
                    Thread.sleep(10);/*单位毫秒*/
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();//释放锁
            }
            //}
        }
    }
}
package threadsafe2;

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

 

死锁(是一种错误)

不要让两个锁嵌套,避免死锁出现

等待唤醒机制

生产者和消费者(等待唤醒机制)

是一个经典的多线程协作的模式,左边为消费者行为,右边为生产者行为

常见方法

 这些方法都是 Object 类的方法,用锁对象调用这些方法

wait方法:让当前线程和锁绑定,释放锁对象(方法调用者)的锁,并使线程进入等待状态,直到用方法唤醒它

notifyAll方法:唤醒跟锁对象(方法调用者)绑定的所有线程

调用规则

1、必须在同步块或同步方法中调用:

wait(), notify(), 和 notifyAll() 方法必须在对象的同步块或同步方法中调用。如果尝试在非同步上下文中调用这些方法,则会抛出 IllegalMonitorStateException 异常。

2、调用线程必须拥有对象的锁:

调用 wait(), notify(), 或 notifyAll() 的线程必须拥有该对象的锁。这意味着它必须是在该对象的同步块或同步方法中执行。

3、锁的释放与重新获取:

  • 当线程调用 wait() 方法时,它会释放该对象的锁,并进入该对象的等待集(wait set)。随后,线程将暂停执行,直到另一个线程调用了该对象的 notify() 或 notifyAll() 方法,并且当前线程重新获得了对象的锁。
  • notify() 方法会唤醒在该对象上等待的单个线程(如果有的话),但不保证是哪个线程会被唤醒。被唤醒的线程不会立即继续执行,它需要在重新获得对象的锁之后才能继续。
  • notifyAll() 方法会唤醒在该对象上等待的所有线程。同样,这些线程在继续执行之前需要重新获得对象的锁。

阻塞队列方式实现

阻塞:

1、put数据时,放不进去,会等待,叫做阻塞

2、take数据时,去除第一个数据,取不到会等待,也叫做阻塞

阻塞队列的方法(put,take等)底层自己带锁,不需要额外加锁

package waitandnotify2;

import java.util.concurrent.ArrayBlockingQueue;

public class Cook extends Thread {
    ArrayBlockingQueue<String> queue;

    public Cook(ArrayBlockingQueue<String> queue){
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true){
            //不断地从阻塞队列中获取面条
            try {
                String food = queue.take();
                System.out.println(food);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
package waitandnotify2;

import java.util.concurrent.ArrayBlockingQueue;

public class Foodie extends Thread{
    ArrayBlockingQueue<String> queue;

    public Foodie(ArrayBlockingQueue<String> queue){
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true){
            //不断地把面条放到阻塞队列中
            try {
                queue.put("面条");
                System.out.println("厨师放了一碗面条");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
package waitandnotify2;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * 利用阻塞队列完成生产者和消费者代码
 * 细节:生产者和消费者必须使用同一个阻塞队列
 */
public class ThreadDemo {
    public static void main(String[] args) {
        //创建阻塞队列对象
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);//指定上限

        //创建线程的对象,并把阻塞队列传递过去
        Cook c = new Cook(queue);
        Foodie f = new Foodie(queue);
        c.start();
        f.start();
    }
}

练习

循环且按顺序打印

package practise.practise6;

/**
 * 同时运行三个线程,分别打印f,j,w,要求循坏并且按顺序打印fjw,用尽可能多的方法
 * 正确答案
 */
public class RightAnswer extends Thread {
    private static int time = 0; // 0: f, 1: j, 2: w
    private static final Object lock = new Object();
    private char[] arr = {'f', 'j', 'w'};

    @Override
    public void run() {
        while (true) {
            synchronized (lock) {
                while (time % 3 != (getId() % 3)) {//如果线程名和打印顺序不符合则等待
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {//线程在等待状态被中断时抛出异常
                        Thread.currentThread().interrupt();//重新设置线程的中断状态
                    }
                }
                //直到符合后进行打印
                System.out.println("线程" + getId() + ":" + arr[(int) (getId() % 3)]);
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                time++;
                lock.notifyAll();
            }
        }
    }

    public static void main(String[] args) {
        Thread t1 = new RightAnswer();
        Thread t2 = new RightAnswer();
        Thread t3 = new RightAnswer();

        t1.start();
        t2.start();
        t3.start();
    }

}

模拟抽奖

package practise.practise4_2;

import java.util.ArrayList;
import java.util.Collections;

//线程栈方式
public class MyThread extends Thread {

    ArrayList<Integer> list;

    public MyThread(ArrayList<Integer> list) {
        this.list = list;
    }

    @Override
    public void run() {
        //只用创建一个集合即可,每个线程中会自动创建一个
        ArrayList<Integer> boxList = new ArrayList<>();//1 //2
        while (true) {
            synchronized (MyThread.class) {
                if (list.size() == 0) {
                    System.out.println(getName() + boxList);
                    break;
                } else {
                    //继续抽奖
                    Collections.shuffle(list);
                    int prize = list.remove(0);
                    boxList.add(prize);
                }
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}
package practise.practise4_2;

import java.util.ArrayList;
import java.util.Collections;

public class Test {
    public static void main(String[] args) {
        /*
            有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 {10,5,20,50,100,200,500,800,2,80,300,700};
            创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
            随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
            每次抽的过程中,不打印,抽完时一次性打印(随机)
            在此次抽奖过程中,抽奖箱1总共产生了6个奖项。
                分别为:10,20,100,500,2,300最高奖项为300元,总计额为932元
            在此次抽奖过程中,抽奖箱2总共产生了6个奖项。
                分别为:5,50,200,800,80,700最高奖项为800元,总计额为1835元
        */

        //创建奖池
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);

        //创建线程
        MyThread t1 = new MyThread(list);
        MyThread t2 = new MyThread(list);


        //设置名字
        t1.setName("抽奖箱1");
        t2.setName("抽奖箱2");


        //启动线程
        t1.start();
        t2.start();

    }
}

抢红包

package practise.practise2;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;

//抢红包升级,精确小数点后两位
public class MyThread_Plus extends Thread{

    //总金额
    static BigDecimal money = BigDecimal.valueOf(100.0);
    //个数
    static int count = 3;
    //最小抽奖金额
    static final BigDecimal MIN = BigDecimal.valueOf(0.01);

    @Override
    public void run() {
        synchronized (MyThread.class){
            if(count == 0){
                System.out.println(getName() + "没有抢到红包!");
            }else{
                //中奖金额
                BigDecimal prize;
                if(count == 1){
                    prize = money;
                }else{
                    //获取抽奖范围
                    double bounds = money.subtract(BigDecimal.valueOf(count-1).multiply(MIN)).doubleValue();
                    Random r = new Random();
                    //抽奖金额
                    prize = BigDecimal.valueOf(r.nextDouble(bounds));
                }
                //设置抽中红包,小数点保留两位,四舍五入
                prize = prize.setScale(2,RoundingMode.HALF_UP);
                //在总金额中去掉对应的钱
                money = money.subtract(prize);
                //红包少了一个
                count--;
                //输出红包信息
                System.out.println(getName() + "抽中了" + prize + "元");
            }
        }
    }
}
package practise.practise2;

public class Test {
    public static void main(String[] args) {
        /*
            微信中的抢红包也用到了多线程。
            假设:100块,分成了3个包,现在有5个人去抢。
            其中,红包是共享数据。
            5个人是5条线程。
            打印结果如下:
            	XXX抢到了XXX元
            	XXX抢到了XXX元
            	XXX抢到了XXX元
            	XXX没抢到
            	XXX没抢到
        */

        //创建线程的对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        MyThread t4 = new MyThread();
        MyThread t5 = new MyThread();

        //给线程设置名字
        t1.setName("小A");
        t2.setName("小QQ");
        t3.setName("小哈哈");
        t4.setName("小诗诗");
        t5.setName("小丹丹");

        //启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();




    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值