Java进阶:多线程、synchronized锁

多线程、锁

  • 多线程下数据安全问题
  • 线程安全问题解决方案
  • 死锁
  • 守护线程
  • 定时器
  • 关于Object类中的wait和notify方法。(生产者和消费者模式!)
  • 练习题

多线程下数据安全问题

多线程并发环境下,数据的安全问题(重点)
存在安全问题的三个条件:
1.多线程并发
2.有共享数据
3.共享数据有修改的行为
满足以上3个条件之后,就会存在线程安全问题。

解决方法:线程同步机制,让线程排队执行。

线程同步机制的语法是:

synchronized (){
    // 线程同步代码块
}

synchronized () 括号中传的数据必须是多线程共享的数据。

synchronized出现在实例方法上,一定锁的是this。

public synchronized void 方法名(){}

缺点:这种方式表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。
优点:代码简洁。如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式。

synchronized出现在静态方法上找的是类锁(类锁只有1把)。

public synchronized static void 方法名(){}

对象锁:1个对象1把锁,100个对象100把锁。
类锁:100个对象,也可能只是1把类锁。

Java中有三大变量?【重要的内容。】

  • 实例变量:在堆中。
  • 静态变量:在方法区。
  • 局部变量:在栈中。

以上三大变量中:
局部变量永远都不会存在线程安全问题。局部变量不共享。
方法区都是多线程共享的,所以可能存在线程安全问题。

局部变量、常量:不会有线程安全问题。
成员变量:可能会有线程安全问题。

如果使用局部变量的话,建议使用:StringBuilder(线程不安全,效率高)。因为局部变量不存在线程安全问题。

线程安全问题解决方案

  • 第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
  • 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存不共享,就没有数据安全问题了。
  • 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。

synchronized在开发中不建议嵌套使用,一不小心就很容易出现死锁现象。

死锁

死锁很难调试,并且不会报错。

public class DeadLock {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();

        // t1和t2两个线程共享o1,o2
        MyThread1 t1 = new MyThread1(o1, o2);
        MyThread2 t2 = new MyThread2(o1, o2);

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

class MyThread1 extends Thread {
    Object o1;
    Object o2;
    public MyThread1(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run() {
        synchronized (o1){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2){
                System.out.println("MyThread1");
            }
        }
    }
}

class MyThread2 extends Thread {
    Object o1;
    Object o2;
    public MyThread2(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run() {
        synchronized (o2){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1){
                System.out.println("MyThread2");
            }
        }
    }
}

以上代码永远不会输出,且一直在运行中,不会结束。

守护线程

java语言中线程分为两大类:

  • 用户线程
  • 守护线程(后台线程)
    其中具有代表性的就是:垃圾回收线程(守护线程)。

守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。

注意:主线程main方法是一个用户线程。

public class ThreadTest11 {
    public static void main(String[] args) {
        BakDataThread t1 = new BakDataThread();
        t1.setName("t1");

        // 启动线程之前,将线程设置为守护线程
        t1.setDaemon(true);

        t1.start();

        // 主线程,是用户线程
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "-->" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class BakDataThread extends Thread {
    public void run() {
        int i = 0;
        // 死循环
        // 由于该线程是守护者,当用户线程结束,守护线程自动终止(即死循环结束)。
        while (true) {
            System.out.println(Thread.currentThread().getName() + "-->" + (++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

守护线程一般使用在每天00:00的时候系统数据自动备份。
这个需要使用到定时器,并且我们可以将定时器设置为守护线程。每到00:00的时候就备份一次。所有的用户线程结束了,守护线程自动退出,没有必要进行数据备份了。

定时器

作用:间隔特定的时间,执行特定的程序。

定时器实现方式:

  1. 可以使用sleep方法,睡眠,设置睡眠时间,到这个时间点醒来,执行任务。这种方式是最原始的定时器。(比较low)

  2. 在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。(目前很少使用)

在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。

/*
使用定时器指定定时任务。
方法:
    void schedule(TimerTask task, Date firstTime, long period)
    安排指定的任务在指定的时间开始进行重复的固定延迟执行。
 */
public class TimerTest {
    public static void main(String[] args) throws Exception {
        // 创建定时器对象
        Timer timer = new Timer();
        //Timer timer = new Timer(true);    // 守护线程方式

        // 指定定时任务
        //timer.schedule(定时任务,第一次执行时间,间隔多久执行一次);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date fistTime = sdf.parse("2022-04-18 11:15:00");// 字符串转换成日期
        timer.schedule(new LogTimerTask(),fistTime,1000*10);
    }
}

// 编写一个记录日志的定时任务类
class LogTimerTask extends TimerTask {
    @Override
    public void run() {
        // 需要执行的任务
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String strTime = sdf.format(new Date());
        System.out.println(strTime + "成功完成一次数据备份");
    }
}
  1. FutureTask方式,实现Callable接口。(JDK8新特性。)
    这种方式实现的线程可以获取线程的返回值。
    前两种方式是无法获取线程返回值的,因为run方法返回void。
/*
实现线程的第三种方式:实现Callable接口
    优点:可以获取到线程的执行结果
    缺点:效率比较低
 */
public class ThreadTest12 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 第一步:创建一个“未来任务类”对象
        FutureTask task = new FutureTask(new Callable() {
            // 相当于run方法,只不过call方法有返回值
            @Override
            public Object call() throws Exception {
                // 模拟执行
                System.out.println("call method begin");
                Thread.sleep(1000*3);
                System.out.println("call method end");
                int a = 100;
                int b = 200;
                return a + b; //自动装箱(变成Integer类型)
            }
        });

        // 创建线程对象
        Thread t = new Thread(task);

        // 启动线程
        t.start();

        // 在主线程获取t线程的执行结果
        // get()方法的执行会导致当前线程阻塞
        Object obj = task.get();
        System.out.println("线程执行结果:" + obj);

        // main方法这里的程序想要执行必须等待get()方法结束
        System.out.println("get end");
    }
}

关于Object类中的wait和notify方法。(生产者和消费者模式!)

  1. wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是Object类中自带的。

  2. wait方法和notify方法的使用建立在synchronized线程同步的基础之上。

  3. wait()方法
    Object o = new Object();
    o.wait();
    作用:让正在o对象上活动的当前线程进入等待状态,无期限等待,直到被唤醒为止。(会释放之前占有的o对象的锁)

  4. notify()方法
    Object o = new Object();
    o.notify();
    作用:唤醒正在o对象上等待的线程。(只会通知,不会释放之前占有的o对象的锁)

    还有一个notifyAll()方法:
    这个方法是唤醒o对象上处于等待的所有线程。

/*
    使用wait和notify方法实现”生产者和消费者模式“
    生产一个消费一个(死循环,程序需要手动停止)
 */
public class ThreadTest {
    public static void main(String[] args) {
        // 创建一个仓库对象,共享的
        List list = new ArrayList();
        // 创建两个线程对象
        // 生产者线程
        Thread t1 = new Thread(new Producer(list));
        // 消费者线程
        Thread t2 = new Thread(new Consumer(list));

        t1.setName("生产者线程");
        t2.setName("消费者线程");

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

// 生产线程
class Producer implements Runnable {
    // 仓库
    private List list;

    public Producer(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        // 一直生产
        while (true) {
            //  给仓库对象list加锁
            synchronized (list){
                if (list.size() > 0) {  // >0 表示线程中有一个元素了
                    // 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 仓库为空,进行生产
                Object obj = new Object();
                list.add(obj);
                System.out.println(Thread.currentThread().getName() + "-->" + obj);
                // 唤醒消费者进行消费
                list.notify();
            }
        }
    }
}

// 消费线程
class Consumer implements Runnable {
    // 仓库
    private List list;

    public Consumer(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        // 一直消费
        while (true) {
            synchronized (list) {
                if (list.size() == 0) { // 表示仓库已经空了
                    try {
                        // 消费者线程等待,释放掉list集合的锁
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 仓库有数据,进行消费
                Object obj = list.remove(0);
                System.out.println(Thread.currentThread().getName() + "-->" + obj);
                // 唤醒消费者进行消费
                list.notify();
            }
        }
    }
}

练习题

/*
要求输出:
    t1-->1
    t2-->2
    t1-->3
    t2-->4
    t1-->5
    t2-->6
    ...
 */
public class HomeWork {
    public static void main(String[] args) {
        // 创建一个Num对象
        Num num = new Num(1);
        // 创建两个线程对象
        Thread t1 = new Thread(new T1(num));
        Thread t2 = new Thread(new T2(num));

        t1.setName("t1");
        t2.setName("t2");

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

class Num{
    int i;

    public Num(int i) {
        this.i = i;
    }
}

// t1
class T1 implements Runnable {
    // 仓库
    private Num num;

    public T1(Num num) {
        this.num = num;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (num) {
                if (num.i%2 == 0) {
                    try {
                        num.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
                System.out.println(Thread.currentThread().getName() + "-->" + num.i++);
                // 唤醒t2
                num.notify();
            }
        }
    }
}

// t2
class T2 implements Runnable {
    // 仓库
    private Num num;

    public T2(Num num) {
        this.num = num;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (num) {
                if (num.i%2 == 1) {
                    try {
                        num.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "-->" + num.i++);
                // 唤醒t1
                num.notify();
            }
        }
    }
}

你可能感兴趣的:(Java,java)