黑马程序员技术交流社区

标题: [就业班 2018.11.19] 线程 同步 线程间通信 [打印本页]

作者: Java-yao    时间: 2018-11-20 15:26
标题: [就业班 2018.11.19] 线程 同步 线程间通信
本帖最后由 Java-yao 于 2018-11-26 15:09 编辑

[就业班 2018.11.19]

线程 同步 线程间通信

1. Java中多线程运行原理
随机性:抢占式
CPU执行哪个线程是随机的, 不能人为干预
Java线程调度是抢占式的, 多个线程互相抢夺CPU的执行权

内存:"栈"是每个线程各自的, "堆"是所有线程共用的
多线程情况下, 每个线程都有各自的栈内存
每个线程各自的方法调用, 进的是各自线程的栈

2. 创建多线程程序
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): 让所在线程睡眠指定的毫秒

第一种方式: 继承Thread类
        1. 定义类继承Thread
        2. 重写run()方法, 要执行的任务
        3. 创建子类的对象, 调用start()方法启动线程
[Java] 纯文本查看 复制代码
public class MyThread exctends Thread {  // 定义MyThread类,并继承Thread类
@Override
public void run() { // 重写run方法
// 要执行的任务.不能直接调用
// 继承Thread类后,可以直接使用getName调用获取线程名称
System.out.println(getName()); // 方法一

// 没有继承Thread类 方法二
// Thread currentThread = Thread.currentThread(); // 获取当前执行的线程对象
// String name = currentThread.getName(); // 线程对象调用getname获取线程名称
// System.out.println(name); // 打印线程名称
// 简化合并的写法
System.out.println(Thread.currentThread().getName());
}
}

[Java] 纯文本查看 复制代码
public class Test {  // 定义测试类
    public static void main(String[] args) {
        // 创建2个MyThread对象, 分别调用start()
        // 每个线程对象, 就是一个线程. 2个线程对象就是2个线程
        new MyThread().start();
        new MyThread().start();
        // 获取线程名称, 因为没有继承Thread类, 只能使用对象调用getName方法
        System.out.println(Thread.currentThread().getName());
    }
}


设置线程名称
1. Thread常用方法: setName(String name), Thread(String name)
// 构造方法
Thread(String threadName): 创建Thead对象并指定线程名
// 成员方法
voidsetName(String name): 设置线程名称

2. Thread常用方法: sleep()
// 静态方法
static void sleep(long millis): 让所在线程睡眠指定的"毫秒"(暂停指定的"毫秒")
[Java] 纯文本查看 复制代码
try {
Thread.sleep(1000); // 睡眠1000毫秒. 抛出编译时异常InterruptedException
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

第二种方式: 实现Runnable接口
1. 定义类, 实现Runnable接口
2. 重写 run() 方法, 要执行的代码(任务)
3. 创建Runnable实现类对象 (任务对象)
4. 创建Thread类对象, 在构造方法中传入Runnable实现类对象 (将任务和线程绑定)
5. 通过Thread对象调用 start() 方法启动线程
// 构造方法
Thread Thread(Runnable target): 通过Runnable对象创建Thread对象
Thread Thread(Runnable target, String threadName): 通过Runnable创建对象,指定线程名

// 定义类, 实现Runnable接口. 表示要执行的任务
[Java] 纯文本查看 复制代码
public class RunnableImpl implements Runnable { // 定义类, 实现Runnable接口
// 重写run方法
@Override
public void run() {
        // 要执行的任务
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName + ":" + i);
        }
    }
}


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


Thread和Runnable的区别
实现Runnable的好处:
        1. 避免单继承的局限性
        2. 增强了程序的扩展性, 降低了程序的耦合性(解耦)
线程是Thread, 任务是Runnable实现类对象. 相当于将线程和任务分离
我们追求 "低耦合"

匿名内部类方式创建线程
// 使用匿名内部类, 简化 继承Thread类方式 创建线程
[Java] 纯文本查看 复制代码
new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}.start();


// 使用匿名内部类, 简化 实现Runnable接口方式 创建线程
[Java] 纯文本查看 复制代码
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
new Thread(r).start();


// 简化上面的代码, 直接在Thread的构造方法中, 传递匿名内部类
[Java] 纯文本查看 复制代码
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start();


线程安全问题的原因
多个线程操作共享资源时, 会产生线程安全问题;
问题发生原因:
        JVM是抢占式调度, CPU在每个线程之间切换是随机的, 代码执行到什么位置是不确定的
        在操作共享资源时, 由于一个线程还没有执行完, 另一个线程就来操作, 就会出现问题
如何解决:
        在操作共享资源时, 让线程一个一个来执行, 不要并发操作共享变量, 就可以解决问题
解决多线程操作共享数据的安全问题的3种方式:
        1. 同步代码块
        2. 同步方法
        3. Lock锁机制

解决线程安全问题方式1: 同步代码块
使用 synchronized 关键字修饰的代码块, 并传入一个当作锁的对象
格式:
// 定义一个共享的对象作为锁对象
Objectlock = new Object(); // 锁对象
synchronized (锁对象) {
// 操作共享数据的代码
}
注意:
        锁对象可以是"任意类型的一个对象"
        锁对象必须是"被多个线程共享的唯一的"对象
        锁对象的作用: 只让一个线程在同步代码块中执行

锁对象, 也称为"同步锁", "对象锁", "对象监视器"

同步的原理:
    线程进入同步代码块前, 会"争夺锁对象", "只有一个线程"会抢到锁对象
    进入同步代码块的线程, 会"持有锁对象", 并执行同步代码块中的代码
    此时同步代码块外的线程, 处于"阻塞"状态, 只能等待
    当同步代码块内的线程执行完代码块, 会离开同步代码块, 并"归还锁对象"给同步代码块
    等在同步代码块外的其他线程就可以继续争夺锁对象

解决线程安全问题方式2: 同步方法
使用 synchronized 关键字修饰的方法, 具有默认的锁对象

非静态同步方法的锁对象: this
// 非静态同步方法
publicsynchronizedvoid method(){
// 可能会产生线程安全问题的代码
}

静态同步方法的锁对象: 当前类的字节码对象 Class对象
RunnableImpl.class -> Class对象
获取一个类的字节码对象的3种方式:
        1. 对象名.getClass()                   new RunnableImpl().getClass()
        2. 类名.class                                   RunnableImpl.class
        3. Class.forName("类的全名"); Class.forName("com.itheima.test05.RunnableImpl");
字节码对象的特点: 同一个类, 他的字节码对象只有"唯一的一个"
锁对象必须是多个线程共享的同一个对象

// 静态同步方法
[Java] 纯文本查看 复制代码
public static synchronized void method(){
// 可能会产生线程安全问题的代码
}


解决线程安全问题方式3: Lock锁
java.util.concurrent.locks.Lock接口: JDK 5 新增的Lock接口
java.util.concurrent.locks.ReentrantLock类: Lock的实现类
// 成员方法
voidlock(): 获取锁
voidunlock(): 释放锁

使用方式:
[Java] 纯文本查看 复制代码
public class RunnableImpl implements Runnable {
    // 成员变量创建锁对象, 该锁对象也要所有线程共享唯一一个
    Lock lock = new ReentrantLock();  // 成员变量
    @Override
public void run() {
        // 加锁
        lock.lock();
        try {
            // 操作共享变量的代码...
        } finally {
            // 在finally中保证释放锁
            lock.unlock();  
        }
    }
}


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

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

wait() 和sleep() 的区别:
        1. wait会释放锁, 恢复时需要重新获取锁; sleep不会释放锁
        2. wait可以被notify/notifyAll唤醒; sleep不会
        3. wait要用锁对象调用; sleep要用Thread类名调用

线程的六种生命周期状态:
    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()方法结束(执行结束, 或内部出现异常), 则进入此状态


作者: Java-yao    时间: 2018-11-20 16:19





欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2