并发编程基础——线程

线程

1. 程序、进程、线程

思考什么是程序?什么是进程?什么是线程

  • 程序(prigram):程序是指令和数据的有序集合,也就是我们写的代码,是静态的,
  • 进程:进程是程序执行的过程,是系统分配资源的最小单位
  • 线程:进程中存在最少一个线程(主线程),CPU为进程分配资源,调度线程来使用这些资源,线程是CPU调度的最小单元,程序真正在执行的是线程

Java中的main方法实际上也是一个线程,我们将代码写好,交给这个main线程去处理内存问题,就和我们在写线程的run方法一样

Java内存模型

2. 创建线程的方式

  • 继承Thread类

    • 由于单继承的局限性,不建议使用
  • 实现Runnable接口

    • 避免了单继承的局限性,灵活方便,方便同一个对象被多个线程使用
  • 使用lambda表达式简化操作

public class CreateThreadTest1 {
    //第一种创建方式,声明一个Thread子类,重写run方法
    //第二种方式,带参数构造器传入实现了Runnable接口的对象
    //star()方法将将线程交给CPU调度
    //在A线程中创建B线程,二者优先级相同,会竞争CPU资源
    public static void main(String[] args) {//主线程
        Thread thread1 = new MyThread();
        Thread thread2 = new Thread(new MyRunnable());
        Thread thread3 = new Thread(()->{
            for (int i = 0; i < 30; i++) {
                System.out.println("豆豆不哭" + i);
            }
        });
        thread1.start();
        thread2.start();
        thread3.start();
        //主线程中的方法
        for (int i = 0; i < 3; i++) {
            System.out.println("吃饭"+i);
        }
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("睡觉"+i);
        }
    }
}
class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("打豆豆"+i);
        }
    }
}
//测试结果
/*
	吃饭0
	吃饭1
	吃饭2
	豆豆不哭0
	打豆豆0
	睡觉0
	打豆豆1
	豆豆不哭1
	打豆豆2
	睡觉1
	睡觉2
	豆豆不哭2
*/
  • 龟兔赛跑案例练习
public class TorAndHero {
    public static void main(String[] args) {
        Run run = new Run(100);
        Thread thread1 = new Thread(run,"兔子");
        Thread thread2 = new Thread(run,"乌龟");
        thread1.start();
        thread2.start();
    }
}
class Run implements Runnable{
    String win = null;//胜者
    int length;//跑道长度

    public Run(int length) {
        this.length = length;
    }

    @Override
    public void run() {
        for (int i = 1; i <= length; i++){
            isSleep(i);//兔子中途睡5s,每步需要0.2s,乌龟每步需要0.5s不休息
            if (isGameOver(i))break;//如果win不是空,打印胜利者,循环退出
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
    public boolean isGameOver(int step) {
        if (win!=null) {
            return true;
        }
        if (step>=length){
            win=Thread.currentThread().getName();
            System.out.println("胜利者是" + win);
            return true;
        }
        return false;
    }
    public void isSleep(int step){
        if (Thread.currentThread().getName()=="兔子"){
            if (step==length/2)
                sleep(5000);
            else sleep(200);
        }if (Thread.currentThread().getName()=="乌龟"){
            sleep(500);
        }
    }
    public void sleep(long time){
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3. 并发问题

  • 多个线程同时操作一个对象会发生数据紊乱问题
// 并发问题模拟
public class ConcurrentProblem {
    public static void main(String[] args) {
        BuyRunnable buyRunnable = new BuyRunnable(5);
        new Thread(buyRunnable, "关羽").start();
        new Thread(buyRunnable, "张飞").start();
        new Thread(buyRunnable, "赵云").start();
        new Thread(buyRunnable, "马超").start();
        new Thread(buyRunnable, "黄忠").start();
    }
}
class BuyRunnable implements Runnable {
    private int tickNum;//票数
    public BuyRunnable(int tickNum) {
        this.tickNum = tickNum;
    }

    @Override
    public void run() {
        while (tickNum > 0) {//不能用!=判断,在多线程的情况下会出现死循环!
            System.out.println(Thread.currentThread().getName() + "取得了第" + tickNum-- + "张票");
             try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//测试结果(不一样)
/*
关羽取得了第5张票
赵云取得了第3张票
张飞取得了第4张票
马超取得了第2张票
黄忠取得了第1张票
赵云取得了第0张票
张飞取得了第-1张票
关羽取得了第-1张票
Process finished with exit code 0
*/
  • 产生的原因

Java中都是值传递,系统会给线程分配一块内存,在线程中变量更新不是原子操作,而是分步骤的

从内存中将数据备份到线程私有内存区—>修改备份—>然后用备份覆盖内存中的数据

重复写:多个线程可以同时获得一个数据的备份,都对备份数据执行-1操作 回写相同的结果

跳过判断:如果线程A获得备份时ticktNum是1,在覆盖前b进入循环然后进入睡眠状态,A写回数据成功,而这时b线程已经跳过判断步骤,苏醒后备份拿到的ticktNum是0这个不合法的值,还对这个值进行了--,同理,当线程C苏醒后再获得这个负值(增加线程停止判断)

跳过取:线程独立运行,完成的速度也不相同,资源共享情况下,线程a在修改完数据还没有打印,线程b再获得这个修改后的数据提前完成打印,产生跳过取得线程

总结,多个线程争抢同一个数据会导致数据紊乱,出现各种奇葩现象,这就是线程不安全

思考:为什么龟兔赛跑没有出现这种情况?获取length只是用来比较的,值没有修改和写回的步骤

  • 如何解决?

往下看。。。

5. 线程状态

  • 创建状态 线程被创建new
  • 就绪状态 调用start()方法,等待cpu调度进入运行状态,不调度进入阻塞状态
  • 阻塞状态 调用sleep、wait或同步锁,线程进入阻塞状态
  • 运行状态 cpu调度后执行线程中的run方法代码块
  • 死亡状态 线程结束,不能再次启动

6. 线程控制

  • 利用标志位停止线程

  • 线程休眠

  • 线程礼让

  • 线程强制执行

  • 获取线程当前状态

  • 线程优先级设置

  • 守护线程

7. 线程同步

  • 多个线程操作同一个对象,也叫线程并发

  • 并发会产生数据紊乱问题,这个问题该如何解决呢?——队列+锁

  • 当一个线程需要获取这个资源的时候,需要先获得这个资源的锁,如果无法获得这个资源的锁,那么就进入等待队列(挂起)

  • 线程锁机制synchronized

//两种格式
//1.放在方法名前,在调用方法前获取调用这个方法的this对象锁,也就是this;
//2.在方法中定义并发代码块,指定要获得哪个对象锁(微操)
{
	synchronized(){
        
    }  
}
  • 练习案例
//买票案例
public class UnsafeBuy {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket(100);
        Thread thread1 = new Thread(buyTicket,"线程1");
        Thread thread = new Thread(buyTicket,"线程2");
        Thread thread3 = new Thread(buyTicket,"线程3");
        Thread thread4 = new Thread(buyTicket,"线程4");
        thread.start();
        thread1.start();
        thread3.start();
        thread4.start();
    }
}

class BuyTicket implements Runnable {
    private int ticket;
    private boolean flag = true;

    public BuyTicket(int ticket) {
        this.ticket = ticket;
    }

    @Override
    public void run() {
        while (flag) {
                buy();
        }
    }

    private synchronized void buy() {//买票时会改变票数,在此加锁
        if (ticket >= 0) {
            System.out.println(Thread.currentThread().getName() + "取得了" + ticket--);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            flag = false;
        }
    }
}


//银行取钱案例
public class UnSafeBank {
    public static void main(String[] args) {
        Account account = new Account(110, 100);
        new Bank(account, 10).start();
        new Bank(account, 20).start();
    }
}
class Bank extends Thread {
    int needMoney;
    Account account;
    boolean flag = true;

    public Bank(Account account, int needMoney) {
        this.account = account;//这里不是拷贝这个对象,而是将自己的account指向了这个对象,所以两个线程使用的其实是一个对象
        this.needMoney = needMoney;
    }

    //取钱操作,改变的是账户,给账户加锁
    private void withdrawMoney() {
        synchronized (account){
            System.out.println(account.hashCode());//可以测试一下,两个线程操作的是一个对象哦
            if (account.money >= needMoney) {
                account.money -= needMoney;
                System.out.println(Thread.currentThread().getName() + "--" + needMoney + "——余额" + account.money);
            } else {
                System.out.println(Thread.currentThread().getName() + "need" + needMoney + "余额不足");
                flag = false;
            }
        }
    }
    //测试用的sleep
    public void mySleep(long time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        if (flag) {
            withdrawMoney();
        }
    }
}

class Account {
    int id;
    int money;
    public Account(int id, int money) {
        this.id = id;
        this.money = money;
    }
}

//集合案例
public class UnSafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i=1; i<=10000; i++) {
            new Thread(() -> {
                synchronized (list) {//可能多个线程操作在同一区域写操作产生覆盖,在这一行代码给list加锁
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
       Thread.sleep(100);
        System.out.println(list.size());
    }
}
//JUC中定义了一个线程安全的集合类CopyOnWriteArrayList

总结:

  • synchronized意思是尝试获取括号中的对象的锁,如果获得这个对象的锁,那么执行并发块中代码,并发块执行完毕后释放锁,因为一个对象只有唯一的一个锁,如果别的线程需要执行这段代码,则必须等待这个锁被释放才有可能获得;
  • 当线程在执行到某块代码时会改变共享对象,就不安全了,需要考虑把这段代码放入并发代码块,用synchronized获得这个对象锁,让线程排队,避免数据紊乱
  • synchronized锁机制的缺陷
    • 一个线程持有锁会导致其他所有需要此锁的线程挂起
    • 在多线程竞争的情况下,会产生延时问题
    • 如果一个优先级高的线程等待优先级低的线程,会导致优先级倒置

8. 死锁

什么是死锁?

死锁就是两个以上线程互相请求对方已经占有的资源,导致都无法进行下去的一种现象

死锁产生的四个必要条件

  • 互斥 :对于线程共享的资源,同一时刻只能有一个线程可以使用
  • 请求保持:线程请求不到资源不会停止
  • 不可剥夺:线程不会释放已经获得的资源
  • 形成环路:多个线程之间资源请求形成环路

即有资源a资源b线程1线程2,线程1,2都需要资源a和b,线程1获得a等待b,线程2获得b等待a,但二者都不会释放已经拥有的资源,那么两个线程一直等待资源却不会有结果,就都不可能继续执行下去,形成死锁;

  • 模拟死锁代码
//模拟死锁
public class DeadLock {
    public static void main(String[] args) {
        Mirror mirror = new Mirror();
        Comb comb = new Comb();
        Runnable makeUp = new Makeup(mirror,comb);
        new Thread(makeUp).start();
        new Thread(makeUp).start();
    }
}

class Makeup implements Runnable {
    Mirror mirror;
    Comb comb;
    //标记位,如果获取不到镜子就获取梳子
    boolean flag = true;

    public Makeup(Mirror mirror, Comb comb) {
        this.mirror = mirror;
        this.comb = comb;
    }

    private void doMake() {
        if (flag) {//有镜子
            synchronized (comb) {
                flag = false;
                comb.getComb();
                mySleep(100);
                synchronized (mirror) {
                    mirror.getMirror();
                }
            }
        } else {
            synchronized (mirror) {
                mirror.getMirror();
                mySleep(100);
                synchronized (comb) {
                    flag = false;
                    comb.getComb();
                }
            }
        }
        flag = true;
    }

    private void mySleep(long time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

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

//资源类
class Mirror {
    public void getMirror() {
        System.out.println(Thread.currentThread().getName() + "获得镜子");
    }
}

class Comb {
    public void getComb() {
        System.out.println(Thread.currentThread().getName() + "获得梳子");
    }
}
  • 怎么解决?不要贪心,一次只拿一个资源的锁,不要再同步块中再拿别的资源的锁
			synchronized (comb) {
                flag = false;
                comb.getComb();
                mySleep(100);
            }
            synchronized (mirror) {
                mirror.getMirror();
            }

9. Lock锁

  • lock锁对代码块加锁,加锁和解锁这段区域的代码线程在调用时排队
  • lock锁锁的是lock自己,synchrized锁的是对象
  • lock锁有很多子类,具有更好的扩展性,使用lock锁JVM调度线程花费的时间较少
  • 优先使用顺序:lock锁>同步代码块(在方法内部具体代码加锁,资源已经分配)>同步方法(在方法外加锁,资源还没有分配)
  • 使用格式
try{
    locke.lock();
    //...并发代码
}finally{
    lock.unlock();//代码执行完后必须释放锁
}

问题: 为什么要这样写,为什么在下面的代码里不这样写程序不会结束?

答: finally{}保证了锁一定会被释放,在下面的案例中如果没有finally{},当线程1如果取到了最后一张票时,直接跳出循环,不会执行释放锁的操作,线程2一直停留在lock()请求锁这一步中,不会死亡,也不会执行下面的语句,所以整个程序不会停止;

问题: 如果在while外面lock()会怎么样?

答:那就只有一个线程可以买票了,票买光了第二个线程才能进去,虽然线程安全了,但是程序只有一个线程可用,失去了多线程的意义

lock锁解决买票问题

public class TestLock {
    public static void main(String[] args) {
        LockRun lockRun = new LockRun();
        new Thread(lockRun).start();
        new Thread(lockRun).start();
    }

}

class LockRun implements Runnable {
    private int tickets = 5;
    Lock lock = new ReentrantLock();//定义一个锁

    @Override
    public void run() {
        while (true) {
            try{
                lock.lock();//获得锁
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + tickets--);
                } else{
                    break;
                }
            }finally {
                lock.unlock();//释放锁
            }
        }
    }
}

10. 线程协作

生产者消费者问题

生活中的案例:一个人麦当劳买炸鸡,前台如果有炸鸡就拿走,没有炸鸡要等厨师做好放在前台;

​ 厨师做炸鸡,前台没有炸鸡要做炸鸡,前台有了炸鸡就等人买过了再做炸鸡

有三个对象:生产者(厨师)【只管生产】

​ 消费者(买炸鸡的人)【只管消费】

​ 缓冲区(前台)【存储资源,唤醒线程,让线程等待】

线程通信问题:生产者是个线程,消费者是个线程,缓冲区是它们共享的一个区域,用来存储资源,消费者消耗缓冲区资源后缓冲区唤醒生产者,生产者生产资源放入缓冲区唤醒消费者,当缓冲区满时让生产者等待消费者,当缓存区空时让消费者等待生产者。

线程通信方法:wait()线程等待

​ notify()唤醒一个等待状态的线程

​ notifyAll()唤醒同一个对象上所有等待状态的线程,cpu按照线程优先级调度

这些都是Object类的方法,只能放在同步方法或同步代码块中,否则会会抛出异常lllegalMonitorStateException//非法监视器状态异常

线程通信机制就是等待和唤醒机制;

  • 代码演示

管程法:把资源和操作资源的方法封装在一起,通过一些判断来控制线程等待还是执行

  • 标志位法:通过信号量来判断资源是否存在,使线程交替执行,不暂存资源

  • 缓冲区法:将资源放在一个缓冲区中,通过缓冲区状态来判断哪个线程该执行

  • 注意事项:一个管程只管理一对生产者和消费者,因为wait会释放锁,唤醒可能会跳过获取锁的步骤

//通过缓冲区解决生产者消费者问题——管程法(一个缓冲区管理一对生产者,消费者线程)
public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer(2);//创建一个缓冲区
        SynContainer container1 = new SynContainer(4);//创建第二个缓冲区
        new Thread(new Producer(container1)).start();//第二个缓冲区的生产者
        new Thread(new Consumer(container1)).start();//第二个缓冲区的消费者
        Producer producer = new Producer(container);//创建一个生产者
        Consumer consumer = new Consumer(container);//创建一个消费者
        Thread proThread = new Thread(producer);//创建生产线程
        Thread conThread = new Thread(consumer);//创建消费线程
        proThread.start();//开启线程
        conThread.start();
    }

}

//生产者只负责生产
class Producer implements Runnable {
    //添加一个缓冲区
    private SynContainer synContainer;

    public Producer(SynContainer synContainer) {
        this.synContainer = synContainer;
    }

    @Override
    public void run() {
        //规定生产者生产完五只鸡结束
        for (int i = 1; i <= 5; i++) {//生产者生产的数量要和消费者消费的数量一致,否则会有有一方因为等待资源而发生死锁
            Chicken chicken = new Chicken(i);//生产鸡
            synContainer.push(chicken);   //把鸡放进缓冲区
            System.out.println("生产了第" + i + "只鸡");
        }
    }
}

//消费者只负责消费
class Consumer implements Runnable {
    //添加一个缓冲区
    private SynContainer synContainer;
    public Consumer(SynContainer synContainer) {
        this.synContainer = synContainer;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {//规定消费者消费完五只鸡结束
            System.out.println("消费了第" + synContainer.pop() + "只鸡");
        }
    }
}

//缓冲区
class SynContainer {
    private int capacity;
    private Chicken[] chickens;
    int count = 0;//计数器

    public SynContainer(int capacity) {
        this.capacity = capacity;
        this.chickens = new Chicken[capacity];//不要在上面直接写private Chicken[] chickens = new Chicken[capacity]
        //因为全局int变量是初值是0,相当于new Chicken[0],在变量声明的时候就会给它分配空间,应该在构造器中初始化它
        //当然也可以两个地方都写,但是这样的话在new的时候chickens变量指向构造函数里创建的数组区域,JVM会进行垃圾回收,把全局中创建的数组清理掉
        //所以何必要让垃圾处理器多处理一次呢?
    }

    //缓冲区被放入产品
    public synchronized void push(Chicken chicken) {
        if (count == chickens.length-1) {
            try {
                //如果在同一个缓冲区创建了多个生产者和消费者依然是不安全的,
                // 因为wait让线程停止在wait()这里,同时会释放锁,如果在这时另一个生产者获得了这个锁也停在了这里,当消费者执行notifyAll()后
                //两个线程都会被唤醒且已经跳过了获取锁的步骤,争抢资源,同理,开启多个消费者线程也会这样
                // 解决办法是什么呢?一个缓冲区只能被一对消费者和生产者操作,不要被多个生产、消费者共享,创建另外一个新的缓冲区
                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();
            }
        }
        Chicken chicken = chickens[count-1];//消费者拿到一只鸡
        count--;
        this.notifyAll();
        return chicken;
    }

}

//资源类
class Chicken {
    private int number;

    public Chicken(int number) {
        this.number = number;
    }

    public int getNumber() {
        return number;
    }
}

标志位法;

//信号量法解决消费者生产者问题
public class SignalPC {
    public static void main(String[] args) {
        //看厨师是否做好了包子,做好了再吃,吃完了再做,做完再吃。。。
        BaoZiPu baoZiPu = new BaoZiPu();
        new Cook(baoZiPu).start();
        new Foodie(baoZiPu).start();
    }

}
//厨师
class Cook extends Thread {
    private BaoZiPu baoZiPu;

    public Cook(BaoZiPu baoZiPu) {
        this.baoZiPu = baoZiPu;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {//做五个包子
            baoZiPu.doBaoZi();
        }
    }
}
//吃货
class Foodie extends Thread{
    private BaoZiPu baoZiPu;

    public Foodie(BaoZiPu baoZiPu) {
        this.baoZiPu = baoZiPu;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            baoZiPu.eat();
        }
    }
}
//包子铺(管程)
class BaoZiPu {
    BaoZi baoZi;
    boolean flag;//厨师做好包子为ture,吃货吃完包子为false
    //吃包子
    public synchronized void eat(){
        if (!flag){//没包子时等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        baoZi = null;//垃圾处理器清除包子
        System.out.println("包子吃完了");
        flag = !flag;
        this.notify();//吃完再唤醒厨师做包子
    }
    //做包子
    public synchronized void doBaoZi(){
        if (flag){//有包子时等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        baoZi = new BaoZi();
        System.out.println("包子做好了");
        flag = !flag;
        this.notify();//做好唤醒吃货吃包子
    }
}
//资源类,包子
class BaoZi{
}

11. 线程池

  • 线程池是提前开辟好了几条线程,等线程任务执行完毕之后线程返回线程池,执行线程池的其他任务,而不是被销毁
  • 原来每个任务都要单独开辟一个线程去执行,执行完后线程即销毁,在程序执行过程中频繁地创建和销毁线程会极大影响效率
  • 所有线程在一个线程池中,方便管理
  • 提前创建好线程——把任务丢入线程池执行——所有任务完成时关闭线程池

线程池使用案例

public class ThreadPool {
    public static void main(String[] args) {
        //线程池中的线程执行完之后不会销毁,可以重复使用
        //线程池通过java.util.concurrent.Executors类创建
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        //把任务加到线程池中去执行
        executorService.submit(new MyRunnable());
        executorService.submit(new MyRunnable());
        executorService.submit(new MyRunnable());
        //第二种方法
        executorService.execute(new MyRunnable());
        executorService.execute(new MyRunnable());
        //关闭线程池
        executorService.shutdown();
    }
}
class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

测试结果表明,线程池中的线程可以重用!

扩展——代理模式

Tread类本身就实现了Runnable接口,代理了一个Runnable实例target,它本身的run方法时空的,保证了没有任务的线程可以正常终止,这个target就是我们自己实现的Runnable接口,在进入Tread的run方法的时候,判断,如果target不是空,则调用target的run方法

//java.lang.Thread 
private Runnable target;

@Override
public void run() {
	if (target != null) {
		target.run();
		}
	}

private void init(...Runnable target...) {
 this.target = target;
}

public Thread(Runnable target) {
     init(null, target, "Thread-" + nextThreadNum(), 0);
 }

Java程序并不能直接与操作系统交互开启线程,而是调用一个native修饰的外部方法stat0来开启线程,而调用这个方法的方法是start(),所以开启线程需要使用new Tread().start,所以nerw Tread().run并不是开启线程的方法,虽然产生了结果,但他其实是执行在本线程中的,而不是开启一个新线程去执行这个方法;

start0()是将线程放入等待队列中,等待CPU调度,所以不一定会立即执行这个线程;

//java.lang.Thread
public synchronized void start() {
 ...
boolean started = false;
try {
	start0();
 started = true;
} finally {...}
 ...
}
private native void start0();

学习地址

posted @ 2020-11-24 20:27  _AntCoder  阅读(96)  评论(0)    收藏  举报