一些JavaSE学习过程中的思路整理(三)(主观性强,持续更新中...)

一些JavaSE学习过程中的思路整理(三)(主观性强,持续更新中...)

未经作者允许,不可转载,如有错误,欢迎指正o( ̄▽ ̄)o,安利一位b站的up:楠哥教你学Java,干货比较多

Java线程同步的几种常见情况分析

同步问题针对的是多个线程所共享的资源,下面分几步假设了一个思考的过程:

  • 1.多个线程共享一个静态成员变量,不进行线程同步处理,直接打印这个静态变量,果然出现问题
public class Test1 {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Account account = new Account();
            Thread thread = new Thread(account);
            thread.start();
        }
    }
}

class Account implements Runnable {
    private static int num = 0;

    @Override
    public void run() {
        num++;
        //此时run没有上锁且如果没有睡眠则可能产生错误,但不一定会有错误
        //但是如果加入睡眠则会出现问题,因为num是共享的,某个
        //线程执行完num++后可能被挂起,其他线程再次执行num++以此类推
        //直到最早从挂起恢复到就绪态的那个线程去执行打印操作时
        //num已经不是预期中的数字
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("第" + num + "次访问");
    }
}
  • 2.在1的基础上,对run方法用synchronized关键字修饰,试图上锁实现线程的同步,但是发现打印结果依旧没有变化,五次打印结果依旧出现重复
public class Test1 {
    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            Account account = new Account();
            Thread thread = new Thread(account);
            thread.start();
        }
    }
}

class Account implements Runnable {
    private static int num = 0;

    //单单是用synchronized修饰后虽然该run方法上锁,但是回到主函数中观察
    //我们发现5次循环,每次都new了一个account对象,用新的account去创建
    //Thread类对象,所以每次线程开启后所上锁的是5个独立的run方法,依旧
    //无法做到某个线程在调用run方法时能独占CPU资源的意图(各自锁各自的run方法)
    @Override
    public synchronized void run() {
        num++;
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("第" + num + "次访问");
    }
}
  • 3.为了解决2的问题,将 Account account = new Account(); 向上挪动以下即可
public class Test1 {
    public static void main(String[] args) {
        //只需要将account对象作为公共的对象引用,则它的run方法在被某个
        //线程占用的时候就能实现该线程独占CPU资源的操作,从而确保线程同步
        Account account = new Account();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(account);
            thread.start();
        }
    }
}

class Account implements Runnable {
    private static int num = 0;

    @Override
    public synchronized void run() {
        num++;
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("第" + num + "次访问");
    }
}
  • 4.对于静态方法,即使每次都实例化一个类的对象,依旧拥有线程同步的功能,因为静态方法是属于类的,而不是属于类的实例的,只要该方法上锁,那个调用这个方法的线程就可以在方法调用结束之前独占CPU的资源,而其他想访问这个方法的线程将被阻塞
public class Test1 {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            //这里用了匿名内部类的方式,在run中调用Test1类的静态方法
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    //虽然这里每次都新建了一个类,但是由于test方法是静态方法,是属于类的共享资源
                    //所以每个线程调用上锁的test方法后,可以由该线程独占CPU资源直到方法结束
                    Test1 test1 = new Test1();
                    test1.test();
                }
            });
            thread.start();
        }
    }

    public static synchronized void test() {
        System.out.println(".....start");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
        }
        System.out.println(".......end");
    }
}

由简单到复杂的几种单例模式写法

  • 单线程模式下的单例模式,但是这种在多线程下无法实现线程安全(线程同步)
public class Test1 {
    public static void main(String[] args) {
        //对main来说是调用了SingletonDemo类的静态方法
        SingletonDemo singletonDemo = SingletonDemo.getInstance();
        SingletonDemo singletonDemo1 = SingletonDemo.getInstance();
    }
}

class SingletonDemo {
    //静态成员变量会默认初始化为(0,false,null)
    private static SingletonDemo singletonDemo;

    //构造方法是属于实例的,所以不能是静态的
    private SingletonDemo() {
        System.out.println("创建了SingletonDemo对象实例");
    }
    
    //静态方法只能调用静态变量与方法
    //但是静态方法能够调用构造方法(一定是非静态的),因为静态方法不需要对类进行实例化就能调用
    //而静态方法之所以无法调用非静态方法是因为非静态方法需要实例化后才能调用,而构造方法很特殊,它
    //用于创建一个新的对象引用后返回该引用,即使在静态方法中调用,也不会出现未实例化而无法调用的情况
    public static SingletonDemo getInstance() {
        if (singletonDemo == null)
            singletonDemo = new SingletonDemo();
        return singletonDemo;
    }
}
  • 多线程模式下的单例模式
public class Test1 {
    public static void main(String[] args) {
        //为了创建例子方便,只在堆内存中进行创建对象引用实例并直接开启线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                SingletonDemo.getInstance();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                SingletonDemo.getInstance();
            }
        }).start();
    }
}

class SingletonDemo {
    //静态成员变量会默认初始化为(0,false,null)
    private static SingletonDemo singletonDemo;

    //构造方法是属于实例的,所以不能是静态的
    private SingletonDemo() {
        System.out.println("创建了SingletonDemo对象实例");
    }

    //静态方法只能调用静态变量与方法
    //但是静态方法能够调用构造方法(一定是非静态的),因为静态方法不需要对类进行实例化就能调用
    //而静态方法之所以无法调用非静态方法是因为非静态方法需要实例化后才能调用,而构造方法很特殊,它
    //用于创建一个新的对象引用后返回该引用,即使在静态方法中调用,也不会出现未实例化而无法调用的情况
    public synchronized static SingletonDemo getInstance() {
        if (singletonDemo == null)
            singletonDemo = new SingletonDemo();
        return singletonDemo;
    }
}
  • 双重监测,synchronized 关键字修饰代码块
  1. 线程同步是为了实现线程安全,如果只创建一个对象,那么线程就是安全的
  2. 如果 synchronized 锁定的是多个线程共享的数据(同一个对象),那么线程就是安全的
  3. volatile 的作用使得主内存中的数据对象对线程直接可见,而不用通过工作内存,一般情况下是不可见的,需要通过工作内存对主内存中需要操作的对象进行拷贝
public class Test1 {
    public static void main(String[] args) {
        //为了创建例子方便,只在堆内存中进行创建对象引用实例并直接开启线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                SingletonDemo.getInstance();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                SingletonDemo.getInstance();
            }
        }).start();
    }
}

class SingletonDemo {
    //静态成员变量会默认初始化为(0,false,null)
    private static volatile SingletonDemo singletonDemo;

    //构造方法是属于实例的,所以不能是静态的
    private SingletonDemo() {
        System.out.println("创建了SingletonDemo对象实例");
    }

    //静态方法只能调用静态变量与方法
    //但是静态方法能够调用构造方法(一定是非静态的),因为静态方法不需要对类进行实例化就能调用
    //而静态方法之所以无法调用非静态方法是因为非静态方法需要实例化后才能调用,而构造方法很特殊,它
    //用于创建一个新的对象引用后返回该引用,即使在静态方法中调用,也不会出现未实例化而无法调用的情况
    public static SingletonDemo getInstance() {
        if (singletonDemo == null) {
            //因为这个类一定是唯一的,所以可以通过锁类达到锁定部分代码块的目的
            //但是需要注意,锁定某个类与该类中的具体逻辑无关?
            synchronized (SingletonDemo.class) {
                if (singletonDemo == null)
                    singletonDemo = new SingletonDemo();
            }
        }
        return singletonDemo;
    }
}

死锁的实现与破解

  • 死锁的产生:由于多个线程同时对多个共享的资源进行抢占,导致某一时刻线程运行所需的资源分布在不同的线程中,而对应的多个线程都无法继续执行下去(如完成两个线程都需要对应的两个资源,但是某一时刻甲占了一个,乙占了另一个,都无法继续执行则两个线程都停在原地等待资源,虽然可能永远也等不到...)
public class DeadLockTest {
    public static void main(String[] args) {
        //因为是继承了Runnable接口的实现类,所以不能用匿名内部类的方式调用
        DeadLockRunnable deadLockRunnable1 = new DeadLockRunnable();
        deadLockRunnable1.num = 1;
        DeadLockRunnable deadLockRunnable2 = new DeadLockRunnable();
        deadLockRunnable2.num = 2;
        new Thread(deadLockRunnable1, "张三").start();
        new Thread(deadLockRunnable2, "李四").start();
    }
}

class DeadLockRunnable implements Runnable {
    public int num;
    //对于静态的类变量采用声明时直接调用构造函数初始化的操作
    private static Chopsticks chopsticks1 = new Chopsticks();
    private static Chopsticks chopsticks2 = new Chopsticks();

    @Override
    public void run() {
        if (num == 1) {
            synchronized (chopsticks1) {
                System.out.println(Thread.currentThread().getName() + "拿到筷子一,等待获取筷子二");
                try {
                    Thread.currentThread().sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (chopsticks2) {
                    System.out.println(Thread.currentThread().getName() + "获取筷子二,完成就餐");
                }
            }
        } else {
            synchronized (chopsticks2) {
                System.out.println(Thread.currentThread().getName() + "拿到了筷子二,等待获取筷子一");
                try {
                    Thread.currentThread().sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (chopsticks1) {
                    System.out.println(Thread.currentThread().getName() + "拿到了筷子一,完成就餐");
                }
            }
        }
    }
}

class Chopsticks {}
  • 破解死锁:依旧以上一个两支筷子的代码为例,在第二个线程开启前,使主线程休眠一段时间,让第一个线程充分使用资源并完成运行,这样第二个线程开启后将获得全部的资源
new Thread(deadLockRunnable1, "张三").start();
//小技巧,shift + tab,使得选中部分向左侧移动一个tab
try {
    Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
new Thread(deadLockRunnable2, "李四").start();

使用lambda表达式化简代码

以函数式接口为参数的方法,可以通过写入lambda表达式达到相同的效果,其中lambda表达式结构为()->{},其中()位置为原本函数式接口需要实现的方法的形式参数,{}中为该函数式接口需要实现的方法的方法体,可以使用()中的参数

import java.util.concurrent.TimeUnit;

public class Test1 {
    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "----------start");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-----------end");
        }, "张三").start();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "----------start");
            try {
                //这个方法进一步封装了休眠的方法,使用更加灵活
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-----------end");
        }, "李四").start();
    }
}

JUC包的Lock接口与ReentrantLock(重入锁)

多线程的实现有两种方式,一种是继承Thread类重写run方法的方式,另一种是实现Runnable接口的方式,普遍认为后一种方式更好,因为一定程度上进行了解耦合,将任务的实现与线程分离

示例代码通过实现Runnable接口的方式展示了重入锁的使用(ReentrantLock就像是手动挡的汽车,相比于synchronized使用更为灵活):

public class Test1 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        new Thread(myRunnable, "张三").start();
        new Thread(myRunnable, "李四").start();
    }
}

class MyRunnable implements Runnable {
    private static int num;
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        lock.lock();
        //可以重复上锁
        lock.lock();
        num++;
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "是当前第" + num + "位访客");
        lock.unlock();
        //
        lock.unlock();
    }
}

在上述代码的基础上可以继续实现资源(实现类)和Runnable接口的解耦合(对此我有点不太理解,是因为类没有继承Runnable接口就是解耦合吗?)

public class Test1 {
    public static void main(String[] args) {
        new Thread(()->{
            MyRunnable.count();
        }, "张三").start();
        new Thread(()->{
            MyRunnable.count();
        }, "李四").start();
    }
}

class MyRunnable {
    //设置线程可见可以进一步提高准确度
    private volatile static int num;
    private static Lock lock = new ReentrantLock();

    public static void count() {
        //关于上锁的内容,一定要明确是多个线程共享的资源,否则无效
        lock.lock();
        num++;
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() +
                "是第" + num + "位访客");
        lock.unlock();
    }
}

ReentrantLock 的时限性:设定一个时间供某个线程获得锁,并返回一个boolean值,通过ReentrantLock对象的tryLock方法,tryLock(long time, TimeUnit unit),time指时间数值,unit指时间单位

public class Test1 {
    public static void main(String[] args) {
        TimeLock timeLock = new TimeLock();
        /*
        张三尝试拿锁成功,拿到锁,执行业务代码,休眠5秒钟
        李四尝试6秒内拿锁失败
         */
        new Thread(()->{
            timeLock.lock();
        }, "张三").start();
        new Thread(()->{
            timeLock.lock();
        }, "李四").start();
    }
}

class TimeLock {
    private ReentrantLock reentrantLock = new ReentrantLock();

    public void lock() {
        //尝试在三秒内获得锁
        try {
            //尝试在指定时间内拿锁,拿到就上锁
            if(reentrantLock.tryLock(6, TimeUnit.SECONDS)) {
                System.out.println(Thread.currentThread().getName() + "get lock");
                TimeUnit.SECONDS.sleep(5);
            } else {
                System.out.println(Thread.currentThread().getName() + "not lock");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //如果拿到锁了这里的boolean值就是true,需要解锁
            if (reentrantLock.isHeldByCurrentThread()) {
                reentrantLock.unlock();
            }
        }
    }
}

生产者消费者模式

容器类

public class Container {
    public Hamburger[] array = new Hamburger[6];
    public int index = 0;

    //往柜台放入汉堡需要上锁
    public synchronized void push(Hamburger hamburger) {
        //如果没有空余位置可以放汉堡则每当生产者线程执行到这里就会
        //被暂停(阻塞),由运行态变为就绪态,下一次被分配到CPU时
        //从上次执行的位置开始继续执行,所以这里要用while而不是if
        //因为下一次可能容器依旧没有空闲的位置,每次都需要进行index的判断
        while (index == array.length) {
            try {
                //暂停访问当前资源的线程
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //重启被暂停(阻塞)的线程
        this.notify();
        array[index++] = hamburger;
        System.out.println("生产了一个汉堡" + hamburger);
    }

    //从柜台取出汉堡也需要上锁
    public synchronized Hamburger pop() {
        //每次被阻塞,变为就绪态,再从就绪态变为运行态后都是从上次
        //执行的位置继续执行,所以要while循环每次都要在取出汉堡前检测index
        while (index == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notify();
        System.out.println("消费了一个汉堡" + array[--index]);
        return array[index];
    }
}

汉堡类

public class Hamburger {
    private int id;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Hamburger(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Hamburger{" +
                "id=" + id +
                '}';
    }
}

生产者类

public class Producer {
    private Container container;

    public Producer(Container container) {
        this.container = container;
    }

    public void produce() {
        for (int i = 0; i < 15; i++) {
            Hamburger hamburger = new Hamburger(i);
            //核心在于调用容器类的放入汉堡方法
            this.container.push(hamburger);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

消费者类

public class Consumer {
    private Container container;

    public Consumer(Container container) {
        this.container = container;
    }

    public void consume() {
        for (int i = 0; i < 15; i++) {
            //核心在于调用容器的取出汉堡的方法
            this.container.pop();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

测试类

public class Test1 {
    public static void main(String[] args) {
        Container container = new Container();
        //这里生产者和消费者贡献容器类的实例
        Producer producer = new Producer(container);
        Consumer consumer = new Consumer(container);
        //无论开启多少个生产者或者消费者的线程,它们都是共享容器资源的
        //所以对容器的实例方法上锁就能达到线程同步(线程安全)的需求
        new Thread(()->{
            producer.produce();
        }).start();
        new Thread(()->{
            producer.produce();
        }).start();
        new Thread(()->{
            consumer.consume();
        }).start();
        new Thread(()->{
            consumer.consume();
        }).start();
        new Thread(()->{
            consumer.consume();
        }).start();
    }
}

多线程并发卖票

三个窗口并发出售15张票的例子

资源类(Ticket)

public class Ticket {
    private int surpluConut = 15;
    private int outCount = 0;
    private ReentrantLock reentrantLock = new ReentrantLock();

    public int getSurpluConut() {
        return surpluConut;
    }

    public void sale() {
        reentrantLock.lock();
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //先检查剩余票数
        if(surpluConut == 0)
            System.out.println(Thread.currentThread().getName() + "已售罄");
        else {
            surpluConut--;
            outCount++;
            System.out.println(Thread.currentThread().getName() + "售出第" + outCount + "张票");
            if (surpluConut == 0) System.out.println(Thread.currentThread().getName() + "已售罄");
        }
        reentrantLock.unlock();
    }
}

测试类

public class Test1 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(()->{
            while (ticket.getSurpluConut() > 0) {
                ticket.sale();
            }
        }, "A").start();
        new Thread(()->{
            while (ticket.getSurpluConut() > 0) {
                ticket.sale();
            }
        }, "B").start();
        new Thread(()->{
            while (ticket.getSurpluConut() > 0) {
                ticket.sale();
            }
        }, "C").start();
    }
}

多线程并发卖票的另一种写法,都看明白了你就懂了

资源类

public class Ticket {
    private int surpluConut = 15;
    private int outCount = 0;
    private ReentrantLock reentrantLock = new ReentrantLock();

    public int getSurpluConut() {
        return surpluConut;
    }

    public void sale() {
        while (surpluConut > 0) {
            //这个重入锁要锁在while循环内部,否则其余线程分配到资源后遇到lock被阻塞,将任由第一个获得锁的线程完成所有票的出售
            //而lock放在while内部会使得每次占用锁的线程只会独占while的一轮循环,一轮循环结束后释放锁,其余线程也有机会获得锁
            reentrantLock.lock();
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //先检查剩余票数
            if(surpluConut == 0)
                System.out.println(Thread.currentThread().getName() + "已售罄");
            else {
                surpluConut--;
                outCount++;
                System.out.println(Thread.currentThread().getName() + "售出第" + outCount + "张票");
                if (surpluConut == 0) System.out.println(Thread.currentThread().getName() + "已售罄");
            }
            reentrantLock.unlock();
        }
    }
}

测试类

public class Test1 {
    public static void main(String[] args) {
        var ticket = new Ticket();
        //任务
        Runnable r = () -> {
          ticket.sale();
        };
        //运行机制解耦合
        new Thread(r, "A").start();
        new Thread(r, "B").start();
        new Thread(r, "C").start();
    }
}

原文地址:https://www.cnblogs.com/YLTFY1998/p/14307577.html