多线程
并发与并行, 进程, 线程调度自行百度
线程(thread): 是一个进程中的其中一条执行路径, CPU 调度的最基本调度的单位. 同一个进程中线程可以共享一些内存(堆, 方法区), 每一个线程又有自己的独立空间(栈, 程序计数器). 因为线程之间有共享的内存, 在实现数据共享方面, 比较方便, 但是又因为共享数据的问题, 会有线程安全问题.
当运行 Java 程序时, 其实已经有一个线程了, 那就是 main 线程.
Thread 类
所有的线程对象都必须是 Thread 类或其子类的实例, Java 中通过继承 Thread 类来创建并启动多线程的步骤如下:
定义 Thread 类的子类, 并重写该类的 run()方法, 该 run()方法的方法体就代表了线程需要完成的任务, 因此把 run()方法称为线程执行体.
创建 Thread 子类的实例, 即创建了线程对象
调用线程对象的 start()方法来启动该线程
Runnable 接口
我们还可以实现 Runnable 接口, 重写 run()方法, 然后再通过 Thread 类的对象代理启动和执行我们的线程体 run()方法. 步骤如下:
定义 Runnable 接口的实现类, 并重写该接口的 run()方法, 该 run()方法的方法体同样是该线程的线程执行体.
创建 Runnable 实现类的实例, 并以此实例作为 Thread 的 target 来创建 Thread 对象, 该 Thread 对象才是真正的线程对象.
调用线程对象的 start()方法来启动线程.
例: public class MyRunnable implements Runnable // 定义实现线程类
- MyRunnable mr = new MyRunnable(); // 创建线程对象
- Thread t = new Thread(mr); // 通过 Thread 类的实例, 启动线程
- t.start();
实际上所有的多线程代码都是通过运行 Thread 的 start()方法来运行的. 因此, 不管是继承 Thread 类还是实现 Runnable 接口来实现多线程, 最终还是通过 Thread 的对象的 API 来控制线程的, 熟悉 Thread 类的 API 是进行多线程编程的基础.
tips:Runnable 对象仅仅作为 Thread 对象的 target,Runnable 实现类里包含的 run()方法仅作为线程执行体. 而实际的线程对象依然是 Thread 实例, 只是该 Thread 线程负责执行其 target 的 run()方法.
两种方式的区别
1, 继承的方式有单继承的限制, 实现的方式可以多实现
2, 启动方式不同
3, 继承: 在实现共享数据时, 可能需要静态的
实现: 只要共享同一个 Runnable 实现类的对象即可.
4, 继承: 选择锁时 this 可能不能用,
实现: 选择锁时 this 可以用.
匿名内部类对象来实现线程的创建和启动
- new Thread("新的线程!"){
- @Override
- public void run() {
- for (int i = 0; i < 10; i++) {
- System.out.println(getName()+": 正在执行!"+i);
- }
- }
- }.start();
构造方法
public Thread() : 分配一个新的线程对象.
public Thread(String name) : 分配一个指定名字的新的线程对象.
public Thread(Runnable target) : 分配一个带有指定目标新的线程对象.
public Thread(Runnable target,String name) : 分配一个带有指定目标新的线程对象并指定名字.
线程常用方法
volatile: 修饰变量
变量不一定在什么时候值就会被修改了, 为了总是得到最新的值, volatile 修饰之后那么每次都从主存中去取值, 不会在寄存器中缓存它的值.
守护线程
守护线程有个特点, 就是如果所有非守护线程都死亡, 那么守护线程自动死亡.
调用 setDaemon(true)方法可将指定线程设置为守护线程. 必须在线程启动之前设置, 否则会报 IllegalThreadStateException 异常.
调用 isDaemon()可以判断线程是否是守护线程.
线程安全
线程安全问题的判断
1, 是否有多个线程
2, 这多个线程是否使用共享数据
3, 这些线程在使用共享数据时, 是否有写有读操作
同步代码块
synchronized 关键字可以用于方法中的某个区块中, 表示只对这个区块的资源实行互斥访问. 格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁必须是对象
锁对象可以是任意类型.
多个线程对象必须使用同一把锁. 注意: 在任何时候, 最多允许一个线程拥有同步锁, 谁拿到锁就进入代码块, 其他的线程只能在外等着(BLOCKED).
同步方法
使用 synchronized 修饰的方法, 就叫做同步方法, 保证持有锁线程执行该方法的时候, 其他线程只能在方法外等着
[其他修饰符] synchronized 返回值类型 方法名([形参列表] )[throws 异常列表] {
- // 可能会产生线程安全问题的代码
- }
锁对象不能由我们自己选, 它是默认的:
(1)静态方法: 锁对象是当前类的 Class 对象
(2)非静态方式: this
线程间通信
当 "数据缓冲区" 满的时候,"生产者" 需要 wait, 等着被唤醒;
当 "数据缓冲区" 空的时候,"消费者" 需要 wait, 等着被唤醒.
在一个线程满足某个条件时, 就进入等待状态 (wait()/wait(time)), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify()); 或可以指定 wait 的时间, 等时间到了自动唤醒; 在有多个线程进行等待时, 如果需要, 可以使用 notifyAll() 来唤醒所有的等待线程.
wait: 线程不再活动, 不再参与调度, 进入 wait set 中, 因此不会浪费 CPU 资源, 也不会去竞争锁了, 这时的线程状态即是 WAITING 或 TIMED_WAITING. 它还要等着别的线程执行一个特别的动作, 也即是 "通知 (notify)" 或者等待时间到, 在这个对象上等待的线程从 wait set 中释放出来, 重新进入到调度队列(ready queue) 中
notify: 则选取所通知对象的 wait set 中的一个线程释放;
notifyAll: 则释放所通知对象的 wait set 上的全部线程.
注意:
被通知线程被唤醒后也不一定能立即恢复执行, 因为它当初中断的地方是在同步块内, 而此刻它已经不持有锁, 所以她需要再次尝试去获取锁(很可能面临其它线程的竞争), 成功后才能在当初调用 wait 方法之后的地方恢复执行.
总结如下:
如果能获取锁, 线程就从 WAITING 状态变成 RUNNABLE(可运行) 状态;
否则, 线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态
调用 wait 和 notify 方法需要注意的细节
wait 方法与 notify 方法必须要由同一个锁对象调用. 因为: 对应的锁对象可以通过 notify 唤醒使用同一个锁对象调用的 wait 方法后的线程.
wait 方法与 notify 方法是属于 Object 类的方法的. 因为: 锁对象可以是任意对象, 而任意对象的所属类都是继承了 Object 类的.
wait 方法与 notify 方法必须要在同步代码块或者是同步函数中使用. 因为: 必须要通过锁对象调用这 2 个方法.
等待唤醒机制可以解决经典的 "生产者与消费者" 的问题
要解决该问题, 就必须让生产者线程在缓冲区满时等待 (wait), 暂停进入阻塞状态, 等到下次消费者消耗了缓冲区中的数据的时候, 通知(notify) 正在等待的线程恢复到就绪状态, 重新开始往缓冲区添加数据. 反之亦然
线程生命周期
一, 站在线程的角度上: 5 种
1, 新建: 创建了线程对象, 还未 start
2, 就绪: 已启动, 并且可被 CPU 调度
3, 运行: 正在被调度
4, 阻塞: 遇到了: sleep(),wait(),wait(time), 其它线程的 join(),join(time),suspend(), 锁被其他线程占用等
解除阻塞回到就绪状态: sleep()时间, notify(),wait 的时间到, 加塞的线程结束, 加塞的时间到, resume(), 其他占用锁的线程释放了锁等.
5, 死亡: run()正常结束, 遇到了未处理的异常或错误, stop()
注意:
程序只能对新建状态的线程调用 start(), 并且只能调用一次, 如果对非新建状态的线程, 如已启动的线程或已死亡的线程调用 start()都会报错 IllegalThreadStateException 异常.
二, 站在代码的角度上 6 种
在 java.lang.Thread.State 的枚举类中这样定义
- public enum State {
- NEW,
- RUNNABLE,
- BLOCKED,
- WAITING,
- TIMED_WAITING,
- TERMINATED;
- }
1, 新建 NEW: 创建了线程对象, 还未 start
2, 可运行 RUNNABLE: 可以被 CPU 调度, 或者正在被调度
3, 阻塞 BLOCKED: 等待锁
4, 等待 WAITING:wait(),join()等没有设置时间的, 必须等 notify(), 或加塞的线程结束才能恢复
5, 有时间等待 TIMED_WAITING:sleep(time),wait(time),join(time)等有时间的阻塞, 等时间到了恢复, 或被 interrupt 也会恢复
6, 终止 TERMINATED:run()正常结束, 遇到了未处理的异常或错误, stop()
释放锁操作与死锁
任何线程进入同步代码块, 同步方法之前, 必须先获得对同步监视器的锁定, 那么何时会释放对同步监视器的锁定呢?
1, 释放锁的操作
当前线程的同步方法, 同步代码块执行结束.
当前线程在同步代码块, 同步方法中出现了未处理的 Error 或 Exception, 导致当前线程异常结束.
当前线程在同步代码块, 同步方法中执行了锁对象的 wait()方法, 当前线程被挂起, 并释放锁.
2, 不会释放锁的操作
线程执行同步代码块或同步方法时, 程序调用 Thread.sleep(),Thread.yield()方法暂停当前线程的执行.
线程执行同步代码块时, 其他线程调用了该线程的 suspend()方法将该线程挂起, 该线程不会释放锁 (同步监视器). 应尽量避免使用 suspend() 和 resume()这样的过时来控制线程.
3, 死锁
不同的线程分别锁住对方需要的同步监视器对象不释放, 都在等待对方先放弃时就形成了线程的死锁. 一旦出现死锁, 整个程序既不会发生异常, 也不会给出任何提示, 只是所有线程处于阻塞状态, 无法继续.
来源: https://www.cnblogs.com/Open-ing/p/11953108.html