A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

day06  线程 同步
    昨天我们了解了线程的羁绊概念,今天我们继续来学习线程,主要是线程的实现以及线程的同步问题。
    以下是今天的学习目标:
  • 能够描述Java中多线程运行原理
  • 能够使用继承类的方式创建多线程
  • 能够使用实现接口的方式创建多线程
  • 能够说出实现接口方式的好处
  • 能够解释安全问题的出现的原因
  • 能够使用同步代码块解决线程安全问题
  • 能够使用同步方法解决线程安全问题
  • 能够说出线程6个状态的名称


以下是今天的详细笔记:


线程多线程原理1: 线程执行的随机性
CPU执行哪个线程是随机的, 不能人为干预
Java线程调度是抢占式的, 多个线程互相抢夺CPU的执行权
补充:
第一种方式: 继承Thread类
        定义类继承Thread
        重写run()方法, 要执行的任务
        创建子类的对象, 调用start()方法启动线程

多线程原理2: 多线程的内存
多线程情况下, 每个线程都有各自的栈内存
每个线程各自的方法调用, 进的是各自线程的栈
栈是每个线程各自的, 堆是所有线程共用的
Thread常用方法: getName(), currentThread()
java.lang.Thread类: 表示线程. 实现了Runnable接口
        // 构造方法
    Thread(): 创建Thead对象
    Thread(String threadName): 创建Thead对象并指定线程名
    Thread(Runnable target): 通过Runnable对象创建Thread对象
    Thread(Runnable target, String threadName): 通过Runnable对象创建对象并指定线程名
        // 成员方法
    void run(): 用于让子类重写, 表示该线程要执行的任务.不能直接调用
    void start(): 启动线程, 即让线程开始执行run()方法中的代码
    String getName(): 获取线程的名称
    void setName(String name): 设置线程名称
        // 静态方法
    static Thread currentThread(): 返回对当前正在执行的线程对象的引用
    static void sleep(long millis): 让所在线程睡眠指定的毫秒
5分钟练习: 测试方法
需求:
定义类: MyThread, 继承Thread
        重写run()方法:
                方法内部使用Thread.currentThread()获取当前线程对象, 再调用getName()获取当前线程的名字, 打印出来
定义测试类:
        创建2个MyThread对象, 分别调用start()
代码:
[Java] 纯文本查看 复制代码
public class MyThread extends Thread {
    @Override
    public void run() {
        // 获取当前线程对象
        /*Thread thread = Thread.currentThread();
        // 获取当前线程的名字
        String threadName = thread.getName();
        System.out.println(threadName);*/

        // 更简单的方式:
        System.out.println(Thread.currentThread().getName());
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建2个MyThread对象, 分别调用start()
        // 可以使用匿名对象方式创建直接调用方法
        new MyThread().start();  // Thread-0
        new MyThread().start();  // Thread-1
    }
}

Thread常用方法: setName(), Thread(String name)
java.lang.Thread类: 表示线程. 实现了Runnable接口
        // 构造方法
    Thread(String threadName): 创建Thead对象并指定线程名
    // 成员方法
    void setName(String name): 设置线程名称
Thread常用方法: sleep()
java.lang.Thread类: 表示线程. 实现了Runnable接口
        // 静态方法
    static void sleep(long millis): 让所在线程睡眠指定的"毫秒"
5分钟练习: 测试方法
需求:
定义类: MyThread, 继承Thread
        重写run()方法:
                方法内使用for循环产生 1-60 (包含60)的数值, 每一秒打印一次线程名字和数值, 如:
                        时钟1
                        时钟2
定义测试类:
        创建MyThread对象, 并设置线程的名字为"时钟", 启动线程查看效果
代码:
[Java] 纯文本查看 复制代码
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 60; i++) {
            System.out.println(Thread.currentThread().getName() + i);
            // 睡眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建线程
        MyThread t1 = new MyThread();
        t1.setName("时钟");
        t1.start();
    }
}

创建多线程程序的方式2: 实现Runnable接口
创建线程的第2种方式:
        1. 定义类, 实现Runnable接口
        2. 重写 run() 方法
        3. 创建Runnable实现类对象
        4. 创建Thread类对象, 在构造方法中传入Runnable实现类对象
        5. 通过Thread对象调用start()方法启动线程

java.lang.Thread类: 表示线程. 实现了Runnable接口
        // 构造方法
    Thread Thread(Runnable target): 通过Runnable对象创建Thread对象
    Thread Thread(Runnable target, String threadName): 通过Runnable对象创建对象并指定线程名
补充:
[Java] 纯文本查看 复制代码
// 定义类, 实现Runnable接口. 表示要执行的任务
public class RunnableImpl implements Runnable {
    
    // 重写run方法
    @Override
    public void run() {
        // 要执行的任务
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName + ":" + i);
        }
    }
}

// 测试类
public class Test {
    public static void main(String[] args) {
        // 先创建任务对象
        RunnableImpl run = new RunnableImpl();
        
        // 然后创建Thread线程对象, 并传入线程要执行的任务
        Thread t = new Thread(run);
        // 调用Thread对象的start方法启动线程
        t.start();
    }
}

5分钟练习: 使用第二种方式创建线程
需求:
定义类: RunnableImpl, 实现Runnable接口
        重写run()方法:
                方法中for循环100次, 打印当前线程名字和次数
定义测试类, 在main()方法中:
        创建RunnableImpl对象, 创建Thread对象并传入RunnableImpl对象, 启动线程
        然后在main()方法中也for循环100次, 打印当前线程名字和次数
代码:
[Java] 纯文本查看 复制代码
public class RunnableImpl implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建任务对象
        RunnableImpl run = new RunnableImpl();
        // 创建线程对象, 传入要执行的任务
        Thread t = new Thread(run);
        t.start();

        // 主线程中也循环100次
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

Thread和Runnable的区别
实现Runnable的好处:        1. 避免单继承的局限性        2. 增强了程序的扩展性, 降低了程序的耦合性(解耦)                线程是Thread, 任务是Runnable实现类对象. 相当于将线程和任务分离
匿名内部类方式创建线程
[Java] 纯文本查看 复制代码
public class Test {
    public static void main(String[] args) {
        // 匿名内部类方式
        new Thread(new Runnable(){
            @Override
            public void run() {
                // 要执行的任务
            }
        }).start();
    }
}

5分钟练习: 匿名内部类创建线程需求:
分别使用Thread子类匿名内部类方式, 和Runnable实现类匿名内部类 2种方式, 创建线程对象
        线程对象内部循环100次, 打印线程名字和次数
代码:
[Java] 纯文本查看 复制代码
public class Test {
    public static void main(String[] args) {
        // 继承Thread类, 使用匿名内部类简化
        new Thread("继承Thread类") {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }
        }.start();

        // 实现Runnable接口, 使用匿名内部类简化
        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }
        };
        new Thread(r, "可以设置名字").start();

        // 更简单的方式
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }
        }, "实现Runnable接口").start();
    }
}

线程安全问题模拟电影院卖票: 线程安全问题概述
模拟电影院卖票: 代码实现多个卖票窗口: 多个Thread对象
卖票任务    : Runnable实现类, 重写run()方法
共享的票    : 因为多个Thread对象使用同一个Runnable实现类对象, 所以将票数定义为Runnable实现类的成员变量
[Java] 纯文本查看 复制代码
// 定义卖票任务
public class RunnableImpl implements Runnable {
	
	// 定义多个线程共享的票数变量
	private int ticket = 100;

    @Override
    public void run() {
        // 不知道卖到什么时候结束, 死循环
        while (true) {
            // 当还有票时卖票
            if (ticket > 0) {
            	// 模拟耗时(只是为了让问题出现几率更大, 但并不是sleep导致的安全问题)
            	try {
                    Thread.sleep(50);
            	} catch (InterruptedException e) {
                    e.printStackTrace();
            	}
            
                // 卖票
                System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
}

// 测试类
public class Test {
    public static void main(String[] args) {
        // 创建卖票任务
        RunnableImpl run = new RunnableImpl();
        // 创建3个窗口, 传入同一个任务对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        // 开始卖票
        t0.start();
        t1.start();
        t2.start();
    }
}

5分钟练习: 实现有问题的卖票
需求: 使用多线程模拟电影院卖票
定义类: RunnableImpl, 实现Runnable接口
        定义票数私有成员变量: private int ticket = 100;
        重写run()方法:
                使用while(true)死循环, 内部使用if判断票数大于0时, 卖票
                卖票的动作用打印当前票数模拟, 卖完一张减去一张
定义测试类:
        创建卖票任务RunnableImpl对象
        创建3个Thread对象作为售票窗口, 传入同一个RunnableImpl对象
        调用start()方法开始卖票, 查看问题
代码:
[Java] 纯文本查看 复制代码
public class RunnableImpl implements Runnable {

    // 定义成员变量, 作为共享的票数
    private int ticket = 100;

    @Override
    public void run() {
        // 循环卖票
        while (true) {
            // 当票数大于0时卖票
            if (ticket > 0) {
                // 打印当前卖出的票
                System.out.println(Thread.currentThread().getName()+"卖出第"+ticket+"张票");
                // 票数--
                ticket--;
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建卖票任务
        RunnableImpl run = new RunnableImpl();

        // 创建3个线程作为窗口, 传入同一个任务
        Thread t0 = new Thread(run, "窗口1");
        Thread t1 = new Thread(run, "窗口2");
        Thread t2 = new Thread(run, "窗口3");

        // 开始卖票
        t0.start();
        t1.start();
        t2.start();
    }
}

线程安全问题的原因















问题发生场景:
        多个线程操作共享资源
问题发生原因:
        JVM是抢占式调度, CPU在每个线程之间切换是随机的, 代码执行到什么位置是不确定的
        在操作共享资源时, 由于一个线程还没有执行完, 另一个线程就来操作, 就会出现问题
如何解决:
        在操作共享资源时, 让线程一个一个来执行, 不要并发操作共享变量, 就可以解决问题
[Java] 纯文本查看 复制代码
@Override
    public void run() {
        while (true) {
        
            // 操作ticket的地方就是操作共享变量
            if (ticket > 0) {    // 这一行用ticket进行了判断
                System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
                ticket--;        // 这一行对ticket进行了修改
            }
            
        }
    }

解决线程安全问题方式1: 同步代码块
解决多线程操作共享数据的安全问题的3种方式:
        1. 同步代码块
        2. 同步方法
        3. 锁机制
       
同步代码块: 使用 synchronized 关键字修饰的代码块, 并传入一个当作锁的对象
格式:
    synchronized (锁对象) {
                // 操作共享数据的代码

    }

注意:
        锁对象可以是任意类型的一个对象
        锁对象必须是被多个线程共享的唯一对象
        锁对象的作用: 只让一个线程在同步代码块中执行

补充:
// 同步代码块就类似于房间和房门

房间
+------+
|       |       线程1
|       \ 门锁  线程2
|       |       线程3
+------+

线程进入房间后, 锁上房间, 其他线程就进不来
+------+
|          |
| 线程1|   线程2
|          |   线程3
+------+

线程1做完事情, 打开锁出了门, 其他线程就可以抢着进来
+------+
|       |
|       \线程2
|       |       线程3
+------+
线程1




5分钟练习: 使用同步代码块解决电影院卖票安全问题
需求:
修改代码, 使用同步代码块解决线程安全问题
代码:
[Java] 纯文本查看 复制代码
public class RunnableImpl implements Runnable {

    // 定义成员变量, 作为共享的票数
    private int ticket = 100;

    // 定义多个线程共享的同一个锁对象
//    Object lock = new Object();
    String lock = new String();

    @Override
    public void run() {
        // 循环卖票
        while (true) {

            // 将操作共享数据的代码用同步代码块套住
            synchronized (lock) {
                // 当票数大于0时卖票
                if (ticket > 0) {
                    // 打印当前卖出的票
                    System.out.println(Thread.currentThread().getName()+"卖出第"+ticket+"张票");
                    // 票数--
                    ticket--;
                }
            }
        }
    }

    /*@Override  错误写法
    public void run() {
        synchronized (lock) {
            // 循环卖票
            while (ticket > 0) {
                // 打印当前卖出的票
                System.out.println(Thread.currentThread().getName()+"卖出第"+ticket+"张票");
                // 票数--
                ticket--;
            }
        }
    }*/
}

public class Test {
    public static void main(String[] args) {
        // 创建卖票任务
        RunnableImpl run = new RunnableImpl();

        // 创建3个线程作为窗口, 传入同一个任务
        Thread t0 = new Thread(run, "窗口1");
        Thread t1 = new Thread(run, "窗口2");
        Thread t2 = new Thread(run, "窗口3");

        // 开始卖票
        t0.start();
        t1.start();
        t2.start();
    }
}

同步技术解决线程安全问题的原理
锁对象, 也称为"同步锁", "对象监视器"

同步的原理:
    线程进入同步代码块前, 会争夺锁对象, 只有一个线程会抢到锁对象
    进入同步代码块的线程, 会持有锁对象, 并执行同步代码块中的代码
    此时同步代码块外的线程, 处于阻塞状态, 只能等待
    当同步代码块内的线程执行完代码块, 会离开同步代码块, 并归还锁对象给同步代码块
    等在同步代码块外的其他线程就可以继续争夺锁对象
解决线程安全问题方式2: 同步方法
同步方法: 使用 synchronized 关键字修饰的方法, 具有默认的锁对象

非静态同步方法的锁对象: this

        // 非静态同步方法
        public synchronized void method(){
                // 可能会产生线程安全问题的代码
        }
5分钟练习: 使用同步方法解决卖票问题
需求:
修改代码, 使用同步方法解决卖票问题
代码:
[Java] 纯文本查看 复制代码
public class RunnableImpl implements Runnable {

    // 定义成员变量, 作为共享的票数
    private int ticket = 100;

    @Override
    public void run() {
        // 循环卖票
        while (true) {
            // 调用同步方法
            sellTicket();
        }
    }

    // 同步方法: 卖票
    // 所有操作共享变量的代码, 都要放在同步方法中
    public synchronized void sellTicket() {
        // 当票数大于0时卖票
        if (ticket > 0) {
            // 打印当前卖出的票
            System.out.println(Thread.currentThread().getName()+"卖出第"+ticket+"张票");
            // 票数--
            ticket--;
        }
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建卖票任务
        RunnableImpl run = new RunnableImpl();

        // 创建3个线程作为窗口, 传入同一个任务
        Thread t0 = new Thread(run, "窗口1");
        Thread t1 = new Thread(run, "窗口2");
        Thread t2 = new Thread(run, "窗口3");

        // 开始卖票
        t0.start();
        t1.start();
        t2.start();
    }
}

静态同步方法
静态同步方法:
        public static synchronized void method(){
                // 可能会产生线程安全问题的代码
        }

静态同步方法的锁对象: 当前类的字节码对象

获取一个类的字节码对象的3种方式:
        1. 对象.getClass()
        2. 类名.class
        3. Class.forName("类的全路径");
解决线程安全问题方式3: Lock锁
java.util.concurrent.locks.Lock接口: JDK 5 新增的Lock接口
        // 成员方法
        void lock(): 获取锁
        void unlock(): 释放锁
       
java.util.concurrent.locks.ReentrantLock类: Lock的实现类

使用方式:
[Java] 纯文本查看 复制代码
public class RunnableImpl implements Runnable {
    // 成员变量创建锁对象, 该锁对象也要所有线程共享唯一一个
    Lock lock = new ReentrantLock();

    @Override 
    public void run() {
        // 加锁
        lock.lock(); 
        try {
            // 操作共享变量的代码...
        } finally {
            // 在finally中保证释放锁
            lock.unlock();  
        }
    }
}

线程安全和效率的特点:
        线程安全, 效率低
        线程不安全, 效率高

5分钟练习: 使用Lock锁解决卖票问题
需求:
修改代码, 使用Lock锁解决卖票问题
代码:
[Java] 纯文本查看 复制代码
public class RunnableImpl implements Runnable {

    // 定义成员变量, 作为共享的票数
    private int ticket = 100;
    // 创建Lock锁
    Lock l = new ReentrantLock();

    @Override
    public void run() {
        // 循环卖票
        while (true) {

            // 加锁
            l.lock();
            try {
                // 当票数大于0时卖票
                if (ticket > 0) {
                    // 打印当前卖出的票
                    System.out.println(Thread.currentThread().getName() + "卖出第" + ticket + "张票");
                    // 票数--
                    ticket--;
                }else {
                    break;
                }
            } finally {
                // 在finally中保证释放锁
                l.unlock();
            }

        }
    }

}

public class Test {
    public static void main(String[] args) {
        // 创建卖票任务
        RunnableImpl run = new RunnableImpl();

        // 创建3个线程作为窗口, 传入同一个任务
        Thread t0 = new Thread(run, "窗口1");
        Thread t1 = new Thread(run, "窗口2");
        Thread t2 = new Thread(run, "窗口3");

        // 开始卖票
        t0.start();
        t1.start();
        t2.start();
    }
}

线程的状态线程状态概述
线程的生命周期中, 有哪6种状态

提前了解:
        锁对象, 也称为"同步锁", "对象监视器"
        Object类中关于线程的方法:
java.lang.Object类:
        // 成员方法 (只能通过"锁对象"调用)
        void notify(): 随机唤醒在同一个锁对象上的某一个处于等待状态的线程
        void notifyAll(): 唤醒所有在同一个锁对象上处于等待状态的线程
        void wait(): 让当前线程处于无限等待状态
        void wait(long timeout): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
        void wait(long timeout, int nanos): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
总结:
线程的生命周期中, 可以出现有6种状态:
    1. NEW 新建
       线程被创建, 但没有调用 start() 启动
    2. RUNNABLE 可运行
       调用 start()方法后已启动, 但可能正在执行 run() 方法的代码, 也可能正在等待CPU的调度
    3. BLOCKED 阻塞
       线程试图获取锁, 但此时锁被其他线程持有
    4. WAITING 无限等待
       通过锁对象调用无参的 wait() 进入此状态.
       等待其他线程通过锁对象执行 notify() 或 notifyAll() 才能结束这个状态
    5. TIMED_WAITING 计时等待
       如通过锁对象调用有参的 wait(long millis) 或 sleep(long millis), 则进入此状态.
       直到时间结束之前被其他线程通过锁对象执行notify()或notifyAll()唤醒, 或时间结束自动唤醒
    6. TERMINATED 终止
       run()方法结束(执行结束, 或内部出现异常), 则进入此状态

等待唤醒案例: 生产者消费者问题需求分析
老板(生产者)是一个线程, 该线程的任务是: 生产包子, 然后通知顾客来吃
顾客(消费者)是另外一个线程, 该线程的任务是: 消费包子, 然后让老板做

两个线程之间, 必须顾客要包子, 然后老板做包子, 然后顾客吃包子, 这是相互协作的场景, 所以线程之间要相互进行通信, 告知彼此应该谁来做事
等待唤醒案例: 生产者消费者问题代码实现5分钟练习: 实现等待唤醒机制案例
需求:
定义测试类:
        创建一个Object对象, 作为老板(生产者)和顾客(消费者)共用的锁对象
        创建顾客(消费者)线程:
                使用匿名内部类对象方式, 创建Thread线程, 重写run()方法
                run()方法内部定义死循环, 死循环内部使用同步代码块
                同步代码块内部打印"[顾客]我要吃包子", 然后通过锁对象调用wait()等待, 然后打印"[顾客]开吃"
        创建老板(生产者)线程:
                使用匿名内部类对象方式, 创建Thread线程, 重写run()方法
                run()方法内部定义死循环, 死循环内部使用同步代码块
                同步代码块内打印: "[老板]包子做好了, 来吃吧", 然后通过锁对象调用notify()通知顾客继续去吃
        运行程序查看效果
代码:
[Java] 纯文本查看 复制代码
/*
等待唤醒机制:
    包子铺
 */
public class Test {
    public static void main(String[] args) {
        // 定义共用的锁对象
        Object lock = new Object();

        // 创建顾客线程
        new Thread() {
            @Override
            public void run() {
                // 循环吃包子
                while (true) {
                    // 同步代码块
                    synchronized (lock) {
                        System.out.println("[顾客说]老板! 我要吃1个包子");
                        // 顾客进入等待状态
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 当顾客被唤醒后, 开始吃包子
                        System.out.println("[顾客说]嗯~好吃");
                        System.out.println("---------------");
                    }
                }
            }
        }.start();

        // 老板线程, 做包子
        new Thread() {
            @Override
            public void run() {
                // 循环做包子
                while (true) {
                    // 花5秒做包子
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 同步代码块
                    synchronized (lock) {
                        System.out.println("[老板说]包子做好了, 来吃吧!");
                        // 唤醒顾客
                        lock.notify();
                    }
                }
            }
        }.start();
    }
}

Object类中wait(long timeout)和notifyAll()方法
java.lang.Object类:
        // 成员方法 (只能通过"锁对象"调用)
        void notifyAll(): 唤醒所有在同一个锁对象上处于等待状态的线程
        void wait(long timeout): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
今日API
java.lang.Thread类: 表示线程. 实现了Runnable接口
        // 构造方法
    Thread Thread(): 创建Thead对象
    Thread Thread(String threadName): 创建Thead对象并指定线程名
    Thread Thread(Runnable target): 通过Runnable对象创建Thread对象
    Thread Thread(Runnable target, String threadName): 通过Runnable对象创建对象并指定线程名
        // 成员方法
    void run(): 用于让子类重写, 表示该线程要执行的任务.不能直接调用
    void start(): 启动线程, 即让线程开始执行run()方法中的代码
    String getName(): 获取线程的名称
    void setName(String name): 设置线程名称
        // 静态方法
    static Thread currentThread(): 返回对当前正在执行的线程对象的引用
    static void sleep(long millis): 让所在线程睡眠指定的毫秒

java.lang.Object类:
        // 成员方法 (只能通过"锁对象"调用)
        void notify(): 随机唤醒在同一个锁对象上的某一个处于等待状态的线程
        void notifyAll(): 唤醒所有在同一个锁对象上处于等待状态的线程
        void wait(): 让当前线程处于无限等待状态
        void wait(long timeout): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
        void wait(long timeout, int nanos): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态












3 个回复

倒序浏览
cuipu 来自手机 中级黑马 2018-9-19 22:11:32
沙发
沙发!!!
回复 使用道具 举报
沙发~~~!
回复 使用道具 举报
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马