Java多线程(一)

一、认识进程、线程

1.1 进程

进程是系统进行资源分配和调度的一个基本单位。

进程是系统中独立存在的实体,拥有自己独立的资源拥有自己私有的地址空间

并行指的是同一时刻,多个指令在多台处理器上同时运行。并发指的是同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,看起来就好像多个指令同时执行一样。)

1.2 线程

线程是进程中的一个实体,是系统调度的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源

1.2.1 线程状态

在这里插入图片描述
1、新建状态(New): 当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

2、就绪状态(Runnable): 也被称为“可执行状态”。当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

3、运行状态(Running): 当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

4、阻塞状态(Blocked): 处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

(1)等待阻塞 – 运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

(2)同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

(3)其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

5、死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

二、创建线程的方式

2.1 继承Thread类

public class ThreadDemo extends Thread {
    private int ticket = 3;

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            if (this.ticket > 0) {
                System.out.println(this.getName() + " ticket" + this.ticket--);
            }
        }
    }
    public static void main(String[] args) {
        Thread t1 = new ThreadDemo();
        Thread t2 = new ThreadDemo();
        t1.start();
        t2.start();
    }

}

运行结果:

Thread-1 ticket3
Thread-0 ticket3
Thread-1 ticket2
Thread-1 ticket1
Thread-0 ticket2
Thread-0 ticket1

从运行结果我们可以看到继承Thread类创建的线程之间是互不干扰的,创建的每个线程都卖出了3张票。

2.2 实现Runnable接口

public class MyRunnable implements Runnable {
    private int ticket = 3;
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (this.ticket > 0) {
                System.out.println(Thread.currentThread().getName() + " " + this.ticket--);
            }
        }
    }

    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
    }
}

运行结果:
Thread-0 3
Thread-1 2
Thread-0 1

主线程main创建并启动2个子线程,而且这2个子线程都是基于“runnable这个Runnable对象”而创建的。运行结果是这3个子线程一共卖出了3张票。这说明它们是共享了MyRunnable接口的。

2.3 Thread和Runnable的异同

Thread 和 Runnable 的相同点:
都是“多线程的实现方式”。

Thread 和 Runnable 的不同点:
Thread 是类,而Runnable是接口;Thread本身是实现了Runnable接口的类。

Runnable还可以用于“资源的共享”。即,多个线程都是基于某一个Runnable对象建立的,它们会共享Runnable对象上的资源。

2.4 实现callable接口

这个callable接口需要返回值类型;重写call方法,需要抛出异常;创建执行对象(就是实现callable接口的对象);创建执行服务ExecutorService service = Executors.newFixedThreadPool(3);提交执行Future ret = service.submit(t1);获取结果Boolean r1 = ret.get();关闭服务service.shutdownNow();

三、Thread类及常见方法

3.1 Thread的常见构造方法

在这里插入图片描述

3.2 Thread的几个常见属性

在这里插入图片描述

  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了
  • 线程的中断问题

3.3 线程中断

在这里插入图片描述

四、多线程安全

4.1 线程不安全的原因

4.1.1 原子性

在这里插入图片描述
什么是原子性

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

一条 java 语句不一定是原子的,也不一定只是一条指令

比如刚才我们看到的 n++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU
  2. 进行数据更新
  3. 把数据写回到 CPU

不保证原子性会给多线程带来什么问题

如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。

4.1.2 可见性

在这里插入图片描述
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。

4.1.3 代码顺序性

什么是代码重排序

一段代码是这样的:

  1. 去前台取下 U 盘
  2. 去教室写 10 分钟作业
  3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序

代码重排序会给多线程带来什么问题

刚才那个例子中,单线程情况是没问题的,优化是正确的,但在多线程场景下就有问题了,什么问题呢。可能快递是在你写作业的10分钟内被另一个线程放过来的,或者被人变过了,如果指令重排序了,代码就会是错误的。

4.2 解决线程不安全

4.2.1 synchronized 关键字

synchronized的底层是使用操作系统的mutex lock实现的。

  • 当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中

  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量

4.2.2 volatile 关键字

用volatile修饰的共享变量,可以保证可见性,部分保证顺序性。

五、多线程方法

5.1 wait() 方法

其实wait()方法就是使线程停止运行。

  1. 方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
  2. wait()方法只能在同步方法中或同步块中调用。如果调用wait()时,没有持有适当的锁,会抛出异常。
  3. wait()方法执行后,当前线程释放锁,线程与其它线程竞争重新获取锁

5.2 notify() 方法

notify方法就是使停止的线程继续运行。

  1. 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个呈wait状态的线程。
  2. 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

注意:wait,notify必须使用在synchronized同步方法或者代码块内。

5.3 notifyAll() 方法

上面说了notify方法只是唤醒某一个等待线程,那么如果有多个线程都在等待中怎么办呢,这个时候就可以使用notifyAll方法可以一次唤醒所有的等待线程。

注意:唤醒线程不能过早,如果在 还没有线程在 等待时,过早的唤醒线程,这个时候就会出现先唤醒,再等待的效果了。这样就没有必要在去运行wait方法了。

5.4 wait 和 sleep的区别

其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间。

总结:

  1. wait 之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是 wait 对象上的 monitor lock
  2. sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求。
  3. wait 是 Object 的方法
  4. sleep 是 Thread 的静态方法

5.5

yield:就是让线程由运行状态转变为就绪状态,然后CPU重新调度。
join:合并线程,待该线程执行完后再执行其他线程,其他线程阻塞(可想像为插队) 我们创建的线程在调用join方法之后,主线程就阻塞,直至调用join方法的线程结束

六、多线程案例

6.1 单例模式

public class Singleton {
	private static Singleton instance = null;
	public static Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
	}
}

在单线程环境下这样写可以保证只取得一个实例。但是在多线程的环境下很可能两个线程同时运行到if (instance == NULL)这一句,导致可能会产生两个实例。于是就要在代码中加锁。

public class Singleton {
    private static volatile Singleton instance = null;
    public static Singleton getInstance() {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        return instance;
    }
}

这样写,每次判断是否为空都需要被加锁,如果有很多线程的话,就会造成大量线程的阻塞,效率低。

public class Singleton {
    private static volatile Singleton instance = null;
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

6.2 阻塞式队列

6.2.1 生产者消费者模型

生产者消费者模型就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而是通过阻塞时队列来通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞式队列,消费者也不直接找生产者要数据,而是直接到阻塞式队列中取,阻塞式队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

七、线程和进程

7.1 线程的优点

  1. 创建一个新线程的代价要比创建一个新进程小得多
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

7.2 进程与线程的区别

  1. 进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
  2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
  3. 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
  4. 线程的创建、切换及终止效率更高。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值