七、线程安全问题
八、线程安全问题的解决——Synchronized同步机制
九、线程安全的单例模式之懒汉式
十、死锁问题
十一、线程安全问题的解决——Lock锁
十二、Lock锁和Synchronized的比较
十三、wait()、notify()、notifyAll()
十四、sleep()和wait()的异同
十五、用实现Callable接口的方式创建多线程
十六、使用线程池创建多线程
一、程序、进程、线程
-
**程序(program):**是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
-
**进程(process):**是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程,有它自身的生命周期。
-
**线程(thread):**进程可进一步细化为线程,是一个程序内部的一条执行路径。一个进程可以包含多个线程。
二、并行和并发的概念
-
**并行:**是指同一时刻多个任务同时在运行,是真意义上的同时运行。
-
**并发:**是指多个任务交替使用CPU,从这个时间段上看似乎这些任务在同时运行,但实际上在某一时刻只有一个任务在运行。
三、用继承Thread类的方式创建多线程
步骤:
-
定义一个子类继承Thread类。
-
子类中重写run()方法。
-
在主线程中new一个子类对象。
-
调用该子类对象的start()方法。
下面在main中创建了一个新的线程用于遍历0——50的偶数
package com.hedong;
//自定义一个子类继承Thread类
class FirstThread extends Thread{
//重写run方法
@Override
public void run() {
//遍历输出0——50的偶数
for (int i = 0; i < 50; i++) {
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
/**
-
@author hedong
-
@version 1.0
-
@date 2020/4/8 8:21
*/
public class MyThread {
public static void main(String[] args) {
//创建子类对象,即一个线程对象
FirstThread firstThread=new FirstThread();
//调用线程对相爱那个的start方法
firstThread.start();
}
}
注意:
-
想要启动多线程,必须调用start()方法。如果手动将调用start()方法的地方改为调用run()方法,那么程序将不会启动多线程模式。
-
run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU****调度决定。
-
一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出****异常“IllegalThreadStateException”。
四、用实现Runnable接口的方式创建多线程
步骤:
-
定义一个子类实现Runnable接口。
-
子类中重写Runnable的run()方法。
-
在主线程中new一个子类对象。
-
将子类对象作为实际参数传递给Thread类的含参构造器中创建线程对象。
-
调用该线程对象的start()方法。
package com.hedong;
//自定义一个子类实现Runnable接口
class FirstThread implements Runnable{
//重写run方法
@Override
public void run() {
//遍历输出0——50的偶数
for (int i = 0; i < 50; i++) {
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
/**
-
@author hedong
-
@version 1.0
-
@date 2020/4/8 8:21
*/
public class MyThread {
public static void main(String[] args) {
//创建子类对象
FirstThread firstThread=new FirstThread();
//将子类对象作为一个参数放入Thread的含参构造器中,生成一个线程对象
Thread myThread=new Thread(firstThread);
//调用线程对象的start方法
myThread.start();
}
}
五、 继承方式和实现方式的联系与区别
联系:通过源码可以发现Thread类实际上也实现了Runnable接口。
区别:继承Thread类的方式是将线程代码放在Thread子类的run()方法中。而实现Runnable接口的方式是将线程代码放在Runnable接口子类的run()方法中。
实现Runnable接口的方式的优点:用此种方式实现的多个线程可以共享实现Runnable接口子类中定义的对象。简单来说就是多个线程共用同一份数据资源。
六、线程的生命周期
七、线程安全问题
**问题举例:**假如卡里原先有3000元,A、B两人同时取这张卡里的钱,两人的操作视作两个不同的线程。A打算取2000元,线程A进入if判断3000>2000后发生阻塞,此时B打算也取2000元,并且线程B在线程A发生阻塞的时候顺利取走了2000元,此时卡里只剩1000元,等线程A阻塞结束后继续扣去卡里2000元,这时悲剧发生:卡里最终金额为-1000。
问题的原因: 当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
解决办法: 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。Java对于多线程的安全问题提供了专业的解决方式: 同步机制。
八、线程安全问题的解决——Synchronized同步机制
Java对于多线程的安全问题提供了专业的解决方式:同步机制。同步机制可以分为同步代码块和同步方法。
**1、同步代码块:**代码如下,同步监视器相当于一把锁,这个锁可以为任意的类对象, 《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 若多线程是用实现Runnable接口的方式实现,则考虑使用this充当对象,若多线程是用继承Thread类的方式实现,则考虑使用当前类充当对象。其中需要被同步的代码即为run()方法中的代码。注意:多个线程必须使用同一把锁,即同一个对象。
synchronized (同步监视器){
// 需要被同步的代码;
}
- 同步代码块处理实现Runnable接口方式的线程安全的问题:
//自定义一个子类实现Runnable接口
class FirstThread implements Runnable{
//重写run方法
@Override
public void run() {
//遍历输出0——50的偶数
synchronized(this) {
for (int i = 0; i < 50; i++) {
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
}
- 同步代码块处理继承Thread类方式的线程安全的问题:
//自定义一个子类继承Thread类
class FirstThread extends Thread{
//重写run方法
@Override
public void run() {
synchronized (FirstThread.class){//
for (int i = 0; i < 50; i++) {
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
}
**2、同步方法:**synchronized还可以放在方法声明中,表示整个方法为同步方法。同步方法仍然涉及到同步监视器,只是不用我们自己去声明,当为非静态的同步方法时,同步监视器是this;当为静态的同步方法时,同步监视器是当前类。
- 同步方法处理实现Runnable接口方式的线程安全的问题:
//自定义一个子类实现Runnable接口
class FirstThread implements Runnable{
//重写run方法
@Override
public void run() {
show();
}
private synchronized void show(){//此时同步监视器为:this
for (int i = 0; i < 50; i++) {
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
- 同步方法处理继承Thread类方式的线程安全的问题:
//自定义一个子类继承Thread类
class FirstThread extends Thread{
//重写run方法
@Override
public void run() {
show();
}
private static synchronized void show(){//静态方法,此时同步监视器为当前类:FirstThread.class
for (int i = 0; i < 50; i++) {
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
九、线程安全的单例模式之懒汉式
**单例模式:**保证类在内存中只能有一个对象。单例模式分为 懒汉式和 饿汉式。
-
**懒汉式:**默认不会实例化,什么时候用什么时候创建对象。
-
**饿汉式:**类加载的时候就实例化,并且创建单例对象。
懒汉式
public class Lazy {
private static Lazy lazy;
public static Lazy getInstance(){
//用的时候才去创建
if(lazy == null){
lazy = new Lazy();
}
return lazy;
}
}
饿汉式
public class Hungry{
//私有化构造器
private Hungry(){}
//类加载的时候就实例化,并且创建单例对象
private static final Hungry hungry=new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
线程安全问题:
-
**饿汉式线程安全 :**在线程还没出现之前就已经实例化了,因此饿汉式线程一定是安全的。
-
**懒汉式线程不安全:**因为懒汉式是用的时候才创建,这时可能会发生这种情况:线程A进入getInstance()方法发现没有lazy对象,于是准备创建,而在此时线程B也进入了getInstance()方法,也发现没有lazy对象,于是也创建了一个lazy对象,这样A、B两个线程就创建了两个不同的lazy对象,这就不满足我们的单例模式的要求,因此说线程不安全。
同步机制将懒汉式优化为线程安全的单例模式!!!
优化一:效率稍低,每个线程都需要等着前面的线程释放锁之后才能进去拿着对象出来。
public class Lazy {
private static Lazy lazy;
public static synchronized Lazy getInstance(){//静态方法,此时同步监视器为当前类:Lazy.class
if(lazy == null){
lazy = new Lazy();
}
return lazy;
}
}
优化二:效率高,越靠后的线程越不易等待,前面的线程已经创建好了对象之后,后面的线程只需要在最外层判断一下是否有对象即可,若对象存在的话直接就可以拿着对象走了。
public class Lazy {
private static Lazy lazy;
public static Lazy getInstance(){
if(lazy == null){
synchronized (Lazy.class) {
if(lazy == null)
lazy = new Lazy();
}
}
return lazy;
}
}
十、死锁问题
死锁: 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
**死锁的简单示例:**如下面的死锁代码样例,线程一拿到a锁后发生阻塞,此时线程二拿到b锁发生阻塞,当两个线程阻塞结束后,线程一需要继续拿到b锁,而此时b锁在线程二手里,同样,线程此时需要继续拿到a锁,但a锁在线程一手上,双方这时都占用着对方需要的同步资源不放弃,发生死锁现象。
package com.hedong;
/**
-
@author hedong
-
@version 1.0
-
@date 2020/4/8 15:05
*/
public class DeadLock {
public static String a = “a”;
public static String b = “b”;
public static void main(String[] args){
Thread a = new Thread(new MyThread1());
Thread b = new Thread(new MyThread2());
a.start();
b.start();
}
}
class MyThread1 implements Runnable{
@Override
public void run(){