Java多线程

Java多线程笔记

创建线程的三种方式

  1. 继承Thread类
    • 继承Thread类
    • 使用Thread类对象的start()启动线程
  2. 实现Runnable接口(推荐)
    • 实现Runnable接口
    • 使用Thread类对象代理线程用start()启动线程
  3. 实现Callable接口

第一种方式:继承Thread类

  1. 创建类继承Thread类,重写run()方法

    public class TestThread extends Thread{
        //线程体
        @Override
        public void run() {
            for (int i = 0; i < 200; i++) {
                System.out.println("TestThread:"+i);
            }
        }
    }
    
  2. 主入口

    创建Thread类对象,再调用start()方法就可以启动线程了

    public class Application {
        //主进程
        public static void main(String[] args) {
            //方式一:继承Thread类
            //新建线程对象,再调用start()方法
            new TestThread().start();
        }
    }
    

第二种方式:实现Runnable接口

  1. 创建线程类,实现Runnable接口,重写run()方法

    public class TestRunnable implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 200; i++) {
                System.out.println(Thread.currentThread().getName()+"->"+i);
            }
        }
    }
    

    其中Thread.currentThread().getName()是获得当前线程的线程名,在创建线程对象的时候可以给线程指定线程名

  2. 主入口

    创建Thread类对象,使用代理模式将实现了Runnable接口的线程丢入Thread类对象里,再调用Thread类对象的start()方法启动线程

    public class Application {
        //主进程
        public static void main(String[] args) {
            //方式二:实现Runnable接口
            //新建Thread()代理,参数是线程,再调用start()
            TestRunnable runnable = new TestRunnable();
            //第一个参数是线程,第二个参数是线程名
            new Thread(runnable,"RunnableThread").start();
        }
    }
    

第三种方式:实现Callable接口

public class TestCallable implements Callable<Boolean> {
    @Override
    public Boolean call()  {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"->"+i);
        }
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建线程对象
        TestCallable t1 = new TestCallable();
        TestCallable t2 = new TestCallable();
        TestCallable t3 = new TestCallable();

        //1、开启线程池服务
        ExecutorService service = Executors.newFixedThreadPool(5);

        //2、将线程提交以供执行
        Future<Boolean> s1 = service.submit(t1);
        Future<Boolean> s2 = service.submit(t2);
        Future<Boolean> s3 = service.submit(t3);

        //3、分别获取线程执行的结果
        Boolean r1 = s1.get();
        Boolean r2 = s2.get();
        Boolean r3 = s3.get();

        //4、关闭服务(切记)
        service.shutdown();
    }
}
  1. 实现Callable接口,并且指定返回值类型
  2. 在这个类里重写call()接口,注意区别前两种, 前两种是实现run()方法
  3. 先开启线程池服务,用sumbit()方法将线程提交,然后用get()方法得到线程的返回值
  4. 最后记得关闭线程池服务

了解静态代理模式

举个例子:假如你要结婚了,你需要找婚庆公司帮忙,那么实际上你和婚庆公司都要结婚,但是婚庆公司做得可能会比你做得要多,比如帮你筹备,帮你计划,帮你拍照等待,但是无论如何你们都是要实现同一个目的——>“结婚”,只是婚庆公司会帮你做而且做得更好,所以婚庆公司就是代理,你就是真正要实现的角色。用户只需要关心自己要实现的功能就好了,婚庆公司(代理)帮你做一些锦上添花的事情,但是具体怎么做,你其实并不用操心。

  1. 共同实现的功能(结婚)

    用户和婚庆公司(代理)都需要实现的共同功能——结婚

    public interface Marry {
        //结婚
        void Marry();
    }
    
  2. 角色实现这个功能(用户实现结婚)

    你只需要简单的将你的名字输出就可以了

    public class People implements Marry{
        private String peopleName;
        public People(String peopleName) {
            this.peopleName = peopleName;
        }
        @Override
        public void Marry() {
            System.out.println(peopleName+"结婚了");
        }
    }
    
  3. 代理需要实现这个功能(代理实现结婚)

    婚庆公司不仅帮你实现了结婚,甚至还用了before()after()方法帮你增强了结婚前和结婚后。

    public class WeddingCompany implements Marry {
        private Marry target;
    
        public WeddingCompany(Marry target) {
            this.target = target;
        }
    
        @Override
        public void Marry() {
            before();
            target.Marry();
            after();
        }
    
        private void before() {
            System.out.println("婚庆公司帮忙结婚前");
        }
    
        private void after() {
            System.out.println("婚庆公司帮忙结婚后");
        }
    }
    
  4. 主入口

    需要新建people对象,同时将people对象作为WeddingCompany代理的参数,然后执行接口中的方法。

    public class Main {
        public static void main(String[] args) {
            //新建people对象
            People people = new People("张三");
            //代理帮忙实现接口并且执行方法
            new WeddingCompany(people).Marry();
        }
    }
    
  5. 总结:

    1. 定义一个接口,声明方法
    2. 目标类实现这个接口,重写里面的方法
    3. 代理类也要实现这个接口,在代理的构造方法中,需要另外的共同实现同一接口的对象作为构造函数target,同时在重写的方法里执行目标对象target的方法和代理的增强。
    4. AOP面向切面编程应该就是静态代理的强化,降低耦合。一个代码只需要专注自己的事情,另外需要对这段代码增强就直接使用静态代理就好了。

由于开启线程或者创建线程的Thread类里实际上就是实现了Runnable接口,同时创建线程的第二种方式也是实现Runnable接口。所以Thread类就是代理,实现Runnable接口的类就是角色。启动线程就是通过代理模式完成的。

lambda表达式

在平常情况下,我们想要实现接口,可以用implements实现它,同时还有一种快捷但是不那么已读的方式,就是使用匿名内部类来临时实现接口,比如:

  • 接口

    public interface UserService {
        void print(String name,String password);
    }
    
  • 匿名内部类

    我们在创建UserService这个对象的时候,直接在后面跟上{}并且重写里面的方法就可以临时实现这个接口了,但是这个实现类并没有具体的类名,所以这就是匿名内部类

    		UserService service = new UserService() {
                @Override
                public void print(String name, String password) {
                    System.out.println("name="+name);
               	 	System.out.println("password="+password);
                }
            };
    

    就有点像Android开发里的按钮监听事件,也是可以用匿名内部类的方式来实现的

    		setOnClickListener(new OnClickListener(){  
                @Override  
                public void onClick(View v) {  
                    // TODO Auto-generated method stub  
                    Intent intet = new Intent(MainActivity.this,seekbar.class);  
                    startActivity(intet);  
                    //finish();  
                }  
                  
            });  
    
  • 接口只需要实现一个方法

    如果接口中只需要实现一个方法的话,那么那么多的匿名内部类的声明代码好像就可以不用写了,因为这个接口本身就只有这一个方法,无论怎样实现都指向这个方法,所有就有了lambda表达式

    		userService=(name, password) -> {
                System.out.println("name="+name);
                System.out.println("password="+password);
            };
            userService.print("名字","密码");
    

    可以看到,我们只需要在实现这个接口的时候用(name,password)传递参数,->{}表明这是一个lambda表达式,中间放实现体就可以了。由于创建线程的方式就是实现Runnable接口里面就只需要重写run()方法,所以我们很多时候就需要用lambda表达式来创建线程。

lambda表达式结合代理模式

所以综上所述,实际上我们可以利用代理模式并且使用lambda表达式来快速创建线程

  1. 创建Thread静态代理

    public class TestLambda {
        public static void main(String[] args) {
            //静态代理
            new Thread();
        }
    }
    
  2. 利用lambda表达式快速创建线程,同时给代理加上start()启动线程

    public class TestLambda {
        public static void main(String[] args) {
            //lambda表达式快速创建线程
            Runnable runnable = ()->{
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+"->"+i);
                }
            };
            new Thread(runnable).start();
        }
    }
    
  3. 由于静态代理需要的参数一定是实现Runnable类对象,所以还可以简化成:

    public class TestLambda {
        public static void main(String[] args) {
            new Thread(()->{
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+"->"+i);
                }
            }).start();
        }
    }
    

    直接在new Thread里写lambda表达式。

线程停止

Java已经不再建议使用传统的stop()destory()方法来停止线程,而是建议利用外部标识位来手动停止线程

  • 线程

    标识位flag默认是true,所以while会一直循环,但是如果外部执行了stop()方法的话那么flag就会变成false,while循环就停止了

    class MyThread implements Runnable{
    
        private boolean flag=true;
    
        @Override
        public void run() {
            int i=0;
            while (flag){
                System.out.println(Thread.currentThread().getName()+"->"+i++);
            }
        }
    
        //置标识位为false
        public void stop(){
            flag=false;
        }
    }
    
  • 主入口

    首先创建线程对象然后启动线程,利用循环来判断,如果循环到了900那么就执行线程里的stop()方法:置线程里的标识位为false从而达到从外部手动停止线程的目的

    public class TestStop {
        public static void main(String[] args) {
            //创建线程对象
            MyThread myThread = new MyThread();
            //启动线程
            new Thread(myThread).start();
            for (int i = 0; i < 1000; i++) {
                System.out.println(Thread.currentThread().getName()+"->"+i);
                if (i==900){//如果i循环到了900则执行线程的停止方法
                    myThread.stop();
                    System.out.println("线程停止");
                }
            }
        }
    }
    

线程休眠(sleep)

线程休眠就是直接调用Thread类的sleep(long millis)方法就好了,参数是毫秒,需要捕获InterruptedException异常,然后线程就会休眠了。

线程礼让(yield)

线程礼让就是在线程体中调用Thread类的yield()方法,注意“礼让”不代表这个线程一定停止运行,而是有可能让另外个线程运行。

线程强制执行(join)

线程强制执行就是调用Thread类中join()方法,让线程插队。

  • 线程

    class JoinThread implements Runnable{
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+"->"+i);
            }
        }
    }
    
  • 主入口

    先创建Thread对象,并且给线程名赋为vip,在for循环里主线程也要打印i,同时当i循环到20的时候,调用Thread类对象的join方法强制执行自定义的线程

    public class TestJoin {
        public static void main(String[] args) throws InterruptedException {
            //创建线程对象
            Thread thread = new Thread(new JoinThread(), "vip");
            //启动线程
            thread.start();
            for (int i = 0; i < 100; i++) {
                //循环数到了20后自定义线程插队
                if (i==20){
                    thread.join();
                }
                //main主线程打印
                System.out.println("main->"+i);
            }
        }
    }
    
  • 观察结果

    通过观察运行结果我们可以指导,在i<20之前,main线程和用户自定义线程并行,但是到了20时,用户自定义线程强制执行后就一直执行用户自定义线程了,直到它执行完毕。最后再是主线程执行完毕。

线程状态(State)

在这里插入图片描述

获取线程状态

线程的状态对应的有5个:

线程状态Java常量
newThread.State.NEW
就绪Thread.State.RUNNABLE
阻塞Thread.State.BLOCKED
等待Thread.State.TIMED_WAITING
死亡Thread.State.TERMINATED

可以利用thread.getState()来获得线程的状态,可以利用这个来循环判断线程是否停止,再执行相应的操作。

线程状态应用

  • 创建一个线程

    public class StateThread implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread->"+i);
            }
        }
    }
    
  • 主方法运行

    public class TestState {
        //声明线程状态对象
        private static Thread.State state;
        public static void main(String[] args) {
            //新建线程对象
            Thread thread = new Thread(new StateThread());
    
            //获得线程状态对象
            state=thread.getState();
            System.out.println(state);//NEW
    
            //启动线程
            thread.start();
            //获得新的线程状态对象(更新)
            state=thread.getState();
            //判断目前的线程对象是否等于TERMINATED
            while (state!= Thread.State.TERMINATED){
                //获得新的线程状态对象(更新)
                state=thread.getState();
                System.out.println(state);
            }
            //获得新的线程状态对象(更新)
            state=thread.getState();
            System.out.println(state);
        }
    }
    

可以看到,我用new Thread()创建了一个新的线程对象thread,然后通过thread对象的getState()方法得到了目前线程的状态并且赋给了state对象,同时再通过stateThread.State.TERMINATED进行比对,如果线程状态不为TERMINATED则继续执行循环,这就是一个利用线程状态的例子。

线程优先级(Priority)

线程优先级就是CPU调度线程的优先级,优先级越高CPU就会优先调度,但是优先级越高就不代表它一定被先调度,只是提高权重,被CPU先调度的可能性更高

可以通过Thread线程对象的getPriority()方法获得当前线程的优先级的值(1~10)是整型,同时也可以通过Thread线程对象的setPriority()方法设置优先级,如果没有设置则默认为5,同时Thread类为给线程优先级初始了三个常量:

	/**
     * 线程最小值
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * 线程正常值
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * 线程最大值
     */
    public final static int MAX_PRIORITY = 10;

感受一下设置线程优先级:

  • 创建一个线程

    public class Priority implements Runnable{
        @Override
        public void run() {
            //打印当前线程名以及线程优先级
            System.out.println(Thread.currentThread().getName()+"->"+Thread.currentThread().getPriority());
        }
    }
    

    Thread.currentThread()可以获得当前运行的线程对象;

    Thread.currentThread().getName()可以获得当前运行的线程对象的名字,需要在创建线程对象的时候给线程指定一个线程名(String);

    Thread.currentThread().getPriority()可以获得当前运行的线程对象的优先级的值;

  • 主方法里运行线程

    public class TestPriority {
        public static void main(String[] args) {
            Priority priority = new Priority();
            //创建线程1对象,并且设置线程名为t1
            Thread t1 = new Thread(priority,"t1");
            //创建线程2对象,并且设置线程名为t2
            Thread t2 = new Thread(priority,"t2");
            //创建线程2对象,并且设置线程名为t3
            Thread t3 = new Thread(priority,"t3");
            //创建线程4对象,并且设置线程名为t4
            Thread t4 = new Thread(priority,"t4");
    
            //设置线程优先级为最大值10
            t1.setPriority(Thread.MAX_PRIORITY);
            //设置线程优先级为5
            t2.setPriority(5);
            //设置线程优先级为最小值1
            t3.setPriority(Thread.MIN_PRIORITY);
    
            //启动线程
            t1.start();
            t2.start();
            t3.start();
            t4.start();
        }
    }
    
  • 观察运行结果

    在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

通过观察我们得出结论:线程优先级越高不代表它一定会被先运行,但是它被CPU调度的可能性更大。同时没有用setPriority()方法给线程指定初始值的话默认是5。

守护线程(Daemon)

线程分为用户线程守护线程

main线程和用户自定义的线程一般都是用户线程

虚拟机必须确保用户线程执行完毕

虚拟机不必等待守护线程执行完毕

守护线程可以用来后台记录操作日志,监控内容,垃圾回收等

Thread线程对象的setDaemon(true)方法将线程设置为守护线程

  • 创建用户线程

    public class People implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println("People Alive "+i+" Day");
            }
            System.out.println("People Dead");
        }
    }
    

    循环i然后打印人还活着,直到i结束打印人死了

  • 创建守护线程

    public class God implements Runnable{
        @Override
        public void run() {
            while (true){
                System.out.println("God Bless You");
            }
        }
    }
    

    while为真死循环打印上帝保佑你

  • 创建主方法入口

    public class Main {
        public static void main(String[] args) {
            God god = new God();
            People people = new People();
    
            //创建God线程对象名叫threadGod
            Thread threadGod = new Thread(god);
            //创建People线程对象名叫threadPeople
            Thread threadPeople = new Thread(people);
    
            //将threadGod线程设置为守护线程
            threadGod.setDaemon(true);//true守护线程,false用户线程,线程默认都是false
    
            //启动线程
            threadGod.start();
            threadPeople.start();
        }
    }
    

    通过threadGodsetDaemon()方法将God线程设置为守护线程,People线程则就是默认的用户线程,观察结果我们可以发现,照理说while(true)循环应该永远进行下去,可是当people线程结束之后,god循环也随之结束了,所以我们就可以得出结论:虚拟机不必等待守护线程结束,虚拟机必须保证用户线程结束。就像上帝就像守护线程永远活着,人就像用户线程只会活一段时间。

线程同步(Synchronized)

线程不安全举例

  • 多个人同时买票

    买票线程:

    1. 其中ticketNumber是所有线程操作的公共资源
    2. 定义了一个标识flag使得while循环能够外部停止
    class BuyTicket implements Runnable{
        //限定20张票
        private int ticketNumber=20;
        //定义一个标识使得循环能够结束
        private boolean flag=true;
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            while (flag){
                System.out.println(Thread.currentThread().getName()+"->"+buyTicket());
            }
        }
    
        /**
         * 买票
         * @return 买的是第几张票
         */
        public int buyTicket(){
            if (ticketNumber<=0){//无票
                flag=false;
                return ticketNumber;
            }else {//有票
                return ticketNumber--;
            }
        }
    }
    

    主入口:

    public class TestBuyTicket {
        public static void main(String[] args) {
            BuyTicket buyTicket = new BuyTicket();
            //创建线程对象并且启动线程
            new Thread(buyTicket,"你").start();
            new Thread(buyTicket,"我").start();
            new Thread(buyTicket,"黄牛").start();
        }
    }
    

    运行结果:

在这里插入图片描述

可以看到”我“和”黄牛“买到了同一张第20票,这是不安全的。

  • 银行取钱

    账户(Account)类:

    //账户类
    class Account{
        //账户余额
        static int balance;
    
        public Account(int balance) {
            this.balance=balance;
        }
    }
    

    取钱线程:

    1. 定义了amount为取的金额
    2. 取了钱后accont类对象的balance就减去amount
    3. 分别打印出取钱前后的账户金额
    //取钱线程
    class Withdraw implements Runnable{
        //取出的金额
        private int amount;
        //账户对象
        private Account account;
        public Withdraw(int amount,Account account){
            this.amount=amount;
            this.account=account;
        }
        @Override
        public void run() {
            if (account.balance<=0){
                System.out.println("账户没钱了");
            }else {
                before();
                System.out.println(Thread.currentThread().getName() + "取了" + amount);
                account.balance-=amount;
                after();
            }
        }
        //打印取钱之前的账户余额
        public void before(){
            System.out.println("取前账户余额:"+account.balance);
        }
        //打印取钱之后的账户余额
        public void after(){
            System.out.println("取后账户余额:"+account.balance);
        }
    }
    

    主入口:

    1. 给两个线程分别赋线程名为”女“和”男“
    public class TestBank {
        public static void main(String[] args) {
            //创建账户类对象,余额有500
            Account account = new Account(500);
            //创建取钱线程
            new Thread(new Withdraw(500,account),"女").start();
            new Thread(new Withdraw(100,account),"男").start();
        }
    }
    

    运行结果:

    第一次运行:

    在这里插入图片描述

    第二次运行:

    在这里插入图片描述

    观察运行结果我们可以发现,有时只有女的可以取钱,但是有时男女同时可以取,并且账户余额还变成了负的,他们一共取了100+500=600元,这也是不安全的。

  • List集合
    1. 定义了1000个线程(线程体是用lambda表达式定义的)
    2. 将1000个线程名添加到List集合里
    public class TestList {
        public static void main(String[] args) {
            ArrayList<String> arrayList=new ArrayList<>();
            for (int i = 0; i < 1000; i++) {
                new Thread(()->{
                    arrayList.add(Thread.currentThread().getName());
                }).start();
            }
            System.out.println("集合长度="+arrayList.size());
        }
    }
    

    运行结果:

    在这里插入图片描述

    观察运行结果我们可以发现,集合长度只有563没有1000,照理说全部线程名加入List集合中的话List的长度应该有1000的,说明线程会覆盖掉其他线程。

synchronized关键字定义同步锁

  • 多人买票的同步锁

    以刚刚的例子为例,如果没有定义同步锁,线程去操作ticketNumber(买票)是混乱的,是杂乱的,极有可能导致ticketNumber冲突,所以就需要定义一个锁。synchronized同步锁就好像是一个仓库的钥匙,假如有很多人需要去这个仓库拿东西,但是先进去的人就把门锁上了,后面的人就进不来了,里面的人操作完后出来就会把锁释放,然后有人拿到这个锁就可以进去仓库操作了,然后循环,就不会像之前没有定义同步锁一样所有人都能进仓库操作。

    public class TestBuyTicket {
        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{
        //限定20张票
        private Integer ticketNumber=20;
        //定义一个标识使得循环能够结束
        private boolean flag=true;
        @Override
        public void run() {
            while (flag){
                buyTicket();
            }
        }
    
        //买票,并且给buyTicket方法加上同步锁
        public synchronized void buyTicket(){
            if (ticketNumber<=0){
                flag=false;
                return;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"->"+ticketNumber--);
        }
    }
    

    只需要给buyTicket0方法加上同步锁就可以了

    运行结果:

    在这里插入图片描述

    不会和别人拿票冲突,并且也不会有拿到第0张票的时候

  • 银行取钱同步锁

    //取钱线程
    class Withdraw implements Runnable{
        //取出的金额
        private int amount;
        //账户对象
        private Account account;
        //定义标识
        private boolean flag;
        public Withdraw(int amount,Account account){
            this.amount=amount;
            this.account=account;
            this.flag=true;
        }
        @Override
        public void run() {
            while (flag){
                getMoney();
            }
        }
        //取钱
        public void getMoney(){
            //给account对象加上同步锁
            synchronized (account){
                if (account.balance<=0){
                    flag=false;
                    return;
                }
                System.out.println("---------------");
                before();
                System.out.println(Thread.currentThread().getName()+"取了"+amount);
                account.balance-=amount;
                after();
                System.out.println("---------------");
                System.out.println();
            }
        }
        //打印取钱之前的账户余额
        public void before(){
            System.out.println(Thread.currentThread().getName()+"取前账户余额:"+account.balance);
        }
        //打印取钱之后的账户余额
        public void after(){
            System.out.println(Thread.currentThread().getName()+"取后账户余额:"+account.balance);
        }
    }
    

    只需要给account对象加上同步锁,然后在同步锁里执行代码块就行了

    运行结果:

    在这里插入图片描述

    取钱再也不会出现余额变成负数的情况了。因为”男“或者”女“分别取钱的时候另外一个线程是不能对account对象进行操作的,只有等其中一个操作完另外一个线程才能进行操作。

  • CopyOnWriteArrayList安全的集合

    public class TestList {
        public static void main(String[] args) {
            //用CopyOnWriteArrayList定义安全的集合
            CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
            new Thread(()->{
                for (int i = 0; i < 1000; i++) {
                    list.add(Thread.currentThread().getName());
                }
            }).start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(list.size());
        }
    }
    

    这里用了Thread.sleep()才能打印出集合的大小是因为在主线程里线程跟主线程是一起在运行的,如果不加线程休眠的话就可能会导致自定义线程还没有跑完但是主线程里已经打印出大小了。

Lock锁和Synchronized锁

lock锁是显式锁,需要显示加锁,显式解锁,而synchronized是隐式锁;

lock只能给代码块加锁,而synchonized能给代码和方法都加上锁;

加了锁能够解决安全和资源冲突问题,但是会降低线程性能,所以不是一定都要加锁。

lock锁的定义方式是先定义一个ReentrantLock对象

  • 不加锁

    public class TestLock {
        public static void main(String[] args) {
            //新建线程lock对象
            Lock lock = new Lock();
            //新建线程并启动
            new Thread(lock,"你").start();
            new Thread(lock,"我").start();
            new Thread(lock,"他").start();
        }
    }
    class Lock implements Runnable{
        private int ticketNumber=10;
        private boolean flag=true;
        @Override
        public void run() {
            while (flag){
                buyTicket();
            }
        }
        public void buyTicket(){
            if (ticketNumber<=0){
                flag=false;
                return;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"->"+ticketNumber--);
        }
    }
    

    运行结果:

    在这里插入图片描述

    会出现买到0甚至-1票的情况

  • 加锁

    先定义一个私有的ReentrantLock对象取名叫lock,再在需要上锁的代码块前加上lock.lock()方法加锁,再在需要释放锁的地方加上lock.unlock()释放锁,注意需要用try代码块包起来,将lock.lock()放在try里,将lock.unlock()放在finally里

    public class TestLock {
        public static void main(String[] args) {
            //新建线程lock对象
            Lock lock = new Lock();
            //新建线程并启动
            new Thread(lock,"你").start();
            new Thread(lock,"我").start();
            new Thread(lock,"他").start();
        }
    }
    class Lock implements Runnable{
        private int ticketNumber=10;
        private boolean flag=true;
        private ReentrantLock lock=new ReentrantLock();
        @Override
        public void run() {
            while (flag){
                buyTicket();
            }
        }
        public void buyTicket(){
            try{
                lock.lock();
                if (ticketNumber<=0){
                    flag=false;
                    return;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"->"+ticketNumber--);
            }finally {
                lock.unlock();
            }
        }
    }
    

    运行结果:

    在这里插入图片描述

    没有人拿到第0甚至第-1张票,结果是符合我们的预期的。

生产者——消费者问题(线程通信)

假如说有两个线程一个是生产者(producer)另一个是消费者(customer)。

生产者:没有生产东西前,需要通知消费者等待,生产后通知消费者消费。

消费者:在没有东西消费前,需要通知生产者生产,消费后通知生产者生产。

Java提供了几个方法供线程通信

方法名说明
wait()表示线程会一直等待,直到其他线程通知,跟sleep()不同,wait会释放锁。
wait(long timeout)指定等待的毫秒数
notify()通知(唤醒)一个等待的线程
notifyAll()通知(唤醒)同一个对象上所有调用wait方法的线程,优先级高的线程先被调度

以上方法均是Object类的方法,但只能在同步块或者同步方法中使用,否则会抛出异常。

解放方式1:管程法

管程法适用于类似消费者去饭店吃炸鸡,生产者可以在消费者消费的时候生产的问题

  • 生产者:生产资源
  • 消费者:消费资源
  • 缓冲区:存放资源

生产者将生产的资源放入缓冲区,消费者从缓冲区取走资源。如果缓冲区满了生产者就停止生产(等待);如果缓冲区为0则消费者停止消费(等待)。

  1. 生产的产品:

    存放产品的编号

    //生产的产品
    class Product{
        //产品编号
        private int id;
    
        public Product(int id) {
            this.id = id;
        }
    
        public int getId() {
            return id;
        }
    
    }
    
  2. 缓冲区:

    看作有大小限制的存放资源的仓库,在缓冲区里定义存入资源和取出资源的方法(记得加上同步锁),在这两个方法里先做一个是否满或者是否为空的判断,如果为空或者为满则wait()等待,否则消费或者生产了产品后记得用notify()通知另一个线程消费或者生产。

    //缓冲区:存放产品的仓库
    class Buffer{
        
        
        //定义数组大小(缓冲区)
        Product[] products=new Product[10];
        //定义产品的数量
        int number=0;
        
        
        //生产者生产产品,往仓库中放入产品
        public synchronized void push(Product product){
            if (number==products.length){//仓库满了
                //生产等待,等待消费者消费
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //将生产出来的产品放到数组里
            products[number]=product;
            //数量加一
            number++;
            //通知消费者消费
            System.out.println(Thread.currentThread().getName()+"生产了第"+product.getId()+"产品");
            this.notifyAll();
        }
        
        
        //消费者消费产品,从仓库中取出产品
        public synchronized Product pull(){
            if (number==0){//仓库没有产品
                //消费等待,等待生产者生产
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //数量减一
            number--;
            //将仓库的第number个赋给产品准备返回
            Product product = products[number];
            System.out.println(Thread.currentThread().getName()+"消费了第"+product.getId()+"产品");
            //通知生产者生产
            this.notifyAll();
            return product;
        }
    }
    
  3. 生产者线程

    因为在生产者线程里需要对缓冲区进行操作,所以需要提前定义缓冲区,生产产品就是new一个产品对象,并且将编号赋给它,同时丢入缓冲区,以供消费者直接从缓冲区中直接取出产品。

    //生产者:生产产品
    class Producer implements Runnable{
        //定义缓冲区对象,因为生产者需要操作缓冲区
        private Buffer buffer;
    
        public Producer(Buffer buffer) {
            this.buffer = buffer;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 50; i++) {
                //i等于0的时候实际上是生产的第1件产品
                produce(i+1);
            }
        }
    
        //生产
        public void produce(int i){
            //生产产品
            Product product = new Product(i);
            //将生产的产品存入缓冲区
            buffer.push(product);
        }
    }
    
  4. 消费者线程

    因为消费者线程里也需要对缓冲区进行操作,所以需要提前定义缓冲区,消费产品就是从缓冲区取一个产品。

    //消费者:消费商品
    class Consumer implements Runnable{
        //定义缓冲区对象,因为消费者要操作缓冲区
        private Buffer buffer;
    
        public Consumer(Buffer buffer) {
            this.buffer = buffer;
        }
        @Override
        public void run() {
            while (true){
                if (consum()>=50){
                    break;
                }
            }
        }
    
        //消费
        public int consum(){
            //从缓冲区从取出产品
            Product product = buffer.pull();
            return product.getId();
        }
    }
    
  5. 主入口

    需要先定义缓冲区,然后将缓冲区丢给生产者消费者线程,然后分别启动两个线程就可以了

    public class TestPC1 {
        public static void main(String[] args) {
            //新建缓冲区
            Buffer buffer = new Buffer();
            //生产者线程
            Producer producer = new Producer(buffer);
            //消费者线程
            Consumer consumer = new Consumer(buffer);
    
            //启动生产者线程
            new Thread(producer,"生产者").start();
            //启动消费者线程
            new Thread(consumer,"消费者").start();
        }
    }
    
  6. 管程法总结

    1. 定义产品类
    2. 定义缓冲区,缓冲区里应该有缓冲区的大小(对象数组),有向缓冲区存入和从缓冲区取出的两个方法。
    3. 生产者向缓冲区存入的方法里面应该有对产品数量的判断,如果缓冲区的数量满了,线程就应该调用wait()方法进行等待,否则就生产新的产品,产品应该是从该方法的形参中来(其中应该是先将对象赋给数组,然后计数再自加),然后notify()通知消费者。
    4. 消费者从缓冲区取出的方法里也应该有对产品数量的判断,如果缓冲区的数量为0,则线程就调用wait()方法进行等待,否则消费产品(计数先自减,然后再将产品作为返回值),然后notify()通知生产者。
    5. 生产者线程也需要声明缓冲区类对象,因为后续需要对缓冲区进行操作,生产者生产是直接new一个产品,然后将产品存入到缓冲区中
    6. 消费者线程也需要声明缓冲区类对象,因为后续需要对缓冲区进行操作,消费者消费实际就是从缓冲区获取一个产品对象。
    7. 最后主入口里记得把缓冲区对象分别赋给生产者消费者线程。

解决方式2:信号灯法

信号灯法就是手动设置一个标识位来限制生产者或者消费者对资源的访问。信号灯法适用于类似两个孩子玩同一个玩具,生产者消费者必须操作同一个资源,生产者生产时消费者必须等待的问题

  • 产品类,需要定义标志位,生产和消费两个方法

    设置一个标识位默认为false,因为flag为false,所以消费者wait()等待,否则消费者消费,消费者消费完了之后notify()通知生产者生产,同时标识位取反。

    同理,当flag为true的时候,生产者wait()等待,否则生产者生产,生产者生产完了notify()通知消费者消费,标识位取反。

    逻辑就是当flag标识位为true的时候,生产者是wait()等待状态,消费者不是wait()状态,消费者正在消费,消费者消费完了之后notify()通知生产者生产,标识位反制。

    总的来说就是生产者在操作资源的时候消费者不能操作,消费者操作资源的时候生产者不能操作资源,怎么判断生产者或者消费者有没有操作资源呢?就是靠标识位。

    //产品
    class Product{
        //产品名字
        private String name;
        /*
          标识位:
                T:消费者消费,生产者等待
                F:生产者生产,消费者等待
         */
        private boolean flag=false;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        //消费
        public synchronized void consum(){
            //为F,消费者等待
            if (!flag){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"正在消费"+getName());
            //通知生产者生产
            this.notifyAll();
            //改变标志位
            flag=!flag;
        }
    
        //生产
        public synchronized void produce(){
            //为T,生产者等待
            if (flag){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"正在生产"+getName());
            //通知消费者消费
            this.notifyAll();
            //改变标志位
            flag=!flag;
        }
    }
    
  • 生产者线程

    //生产者
    class Producer implements Runnable{
        private Product product;
    
        public Producer(Product product) {
            this.product = product;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                if (i%2==0){
                    product.setName("产品1");
                }else {
                    product.setName("产品2");
                }
                //生产产品
                product.produce();
            }
        }
    }
    
  • 消费者线程

    //消费者
    class Consumer implements Runnable{
        private Product product;
    
        public Consumer(Product product) {
            this.product = product;
        }
    
        @Override
        public void run() {
            while (true){
                //消费产品
                product.consum();
            }
        }
    }
    
  • 主入口

    public class TestPC2 {
        public static void main(String[] args) {
            Product product = new Product();
            //启动两个线程
            new Thread(new Producer(product),"生产者").start();
            new Thread(new Consumer(product),"消费者").start();
        }
    }
    
  • 信号灯法总结:

    1. 定义中间产品,需要在产品里定义标识位属性以及生产和消费方法
    2. 定义消费方法,如果标识位为真则用wait()等待,否则消费然后notify()通知,最后标识位取反
    3. 定义生产方法,如果标识位为假则用wait()等待,否则生产然后notify()通知,最后标志位取反
    4. 在消费者或者生产者线程里只需要用循环生产或者消费就行了。

同步锁(Synchronized)总结

锁就有点像事务,线程拿到锁之后就保存该线程先把这个资源用于自己的操作先做完再把锁释放。

有两种锁,synchronizedlock锁,第一个可以锁方法和代码块,而第二个只可以锁代码块。

线程池

线程池就是可以限定最大同时运行线程的个数

线程池的使用方法:

  1. 创建线程池服务,并且指定线程池大小
  2. 将线程放入线程池服务中
  3. 关闭线程池服务
public class TestPool {
    public static void main(String[] args) {
        //1、创建线程池服务,指定线程池大小,参数是线程池大小
        ExecutorService service = Executors.newFixedThreadPool(5);
        //2、将线程放入线程池服务中
        service.execute(new MyThread1());
        service.execute(new MyThread2());
        //3、关闭线程池服务(切记)
        service.shutdown();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值