一文精通Java 多线程之全方位解读
并行和并发
并行:多个CPU实例或是多台机器同时执行一段处理逻辑,是真正的同时。
并发:一个CPU或一台机器,通过CPU调度算法,让用户看上去同时去执行,实际上从CPU操作层面并不是真正的同时。并发往往需要公共的资源,对公共资源的处理和线程之间的协调是并发的难点。
线程基础概念
线程和进程
进程就是程序,有独立的运行内存空间,比如应用和后台服务,windows是一个支持多进程的操作系统。内存越大能同时运行的程序越多,在Java里一个进程指的是一个运行在独立JVM的程序。
线程:一个程序里运行的多个任务,每个任务就是一个线程,线程是共享内存的在QQ、微信、钉钉等软件中每个聊天窗口都是一个线程,可以同时接收消息和发送消息,但只有一个内存占用。
多线程的好处
◆ 同时运行多个任务,提升CPU的使用效率
◆ 共享内存,占用资源更少,线程间可以通信
◆ 异步调用,避免阻塞
◆ 用户体验感更好
线程的状态
线程包括5种状态:
1、新建(New):线程对象被创建时,它只会短暂地处于这种状态。此时它已经分配了必须的系统资源,并执行了初始化。例如,Thread thread = new Thread()。
2、就绪(Runnable):称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
3、运行(Running):线程获取CPU权限进行执行。注意:线程只能从就绪状态进入运行状态。
4、阻塞(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分为三种:
(1)等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成。
(2)同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态。
(3)其他阻塞:通过调用线程的sleep()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或是超时。或是I/O处理完毕时,线程重新转入就绪状态。
5.死亡(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
实现多线程的两种方式
继承Thread类
第一步 继承Therad父类
第二步 重写run方法
第三步 实例化线程类
第四步 启动线程
public class ThreadDemo extends Thread{
@Override
public void run() {
// TODO 自动生成的方法存根
//打印10次世界
try {
for(int i=0;i<10;i++) {
System.out.print("世界");
Thread.sleep(200);//阻塞态 休息200毫秒
}
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
System.out.println("线程执行完成!");
}
public static void main(String[] args) {
//初始化线程t1 t2
ThreadDemo t1 = new ThreadDemo();
ThreadDemo t2 = new ThreadDemo();
//启动线程
t1.start();
t2.start();
//注意不是调用run方法 而是启动线程 调用run方法只是执行了run方法但不是多线程执行
//打印200次你好
for(int i = 0;i<20;i++) {
System.out.print("你好");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
//结束态
System.out.println("程序运行完成");
}
}
注意:多线程每次运行得到的结果是不相同的
Thread.sleep();方法可以使线程阻塞
调用start();方法才能启动多线程
调用run方法和普通方法效果相同
实现Runnable接口
第一步 实现Runnable接口
第二步 重写run方法
第三步 实例化当前类(任务类)
第四步 将任务类对象作为参数传入Thread中进行实列化
第五步 启动多线程
public class RunnableDemo implements Runnable {
@Override
public void run() {
// TODO 自动生成的方法存根
//打印10次世界
try {
for(int i=0;i<10;i++) {
System.out.print(Thread.currentThread().getName()+"世界\t");
if(i%3==0) {
System.out.println();
}
Thread.sleep(200);//阻塞态 休息200毫秒
}
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程执行完成!");
}
public static void main(String[] args) {
//实列化当前类
RunnableDemo runnableDemo1 = new RunnableDemo();
//创建线程 传入任务类
Thread t1 = new Thread(runnableDemo1);
Thread t2 = new Thread(runnableDemo1);
Thread t3 = new Thread(runnableDemo1);
//设置线程优先级
t1.setPriority(Thread.MAX_PRIORITY);//最高优先级
t2.setPriority(Thread.MIN_PRIORITY);//最低优先级
t3.setPriority(5);//设置默认优先级 优先级为5 优先级从0-10最高为10
//启动线程
t1.start();
t2.start();
t3.start();
}
}
注意 setPriority 可以设置线程的优先级
但并不代表线程一定优先执行完
线程的安全性和原子性
由于线程之间可以共享内存,则某个对象(变量)是可以被多个线程共享的,是可以被多个线程同时访问的。当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
举个栗子:甲乙两个人,甲负责向筐里放苹果,乙负责从筐里数苹果,甲乙同时进行,问乙如何操作才能正确?
不幸的是,以上代码不是线程安全的,因为count++并非是原子操作,实际上,它包含了三个独立的操作:读取count的值,将值加1,然后将计算结果写入count。如果线程A读到count为10,马上线程B读到count也为10,线程A加1写入后为11,线程B由于已经读过count值为10,执行加1写入后依然为11,这样就丢失了一次计数。在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它有一个正式的名字:
竞态条件(Race Condition)。
Java 内存模型中的可见性、原子性和有序性。
可见性:当多个线程访问同一个变量x时,线程1修改了变量x的值,线程1、线程2…线程n能够立即读
取到线程1修改后的值。
有序性:即程序执行时按照代码书写的先后顺序执行。在Java内存模型中,允许编译器和处理器对指
令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
原子性:原子性通常指多个操作不存在只执行一部分的情况,要么全部执行、要么全部不执行。
锁的概念和使用
竟态条件会使运行结果变得不可靠,程序的运行结果取决于方法的调用顺序,将方法以串行的
方式来访问,我们称这种方式为同步锁(synchronized)。
Java实现同步锁的方式有:
▶同步方法synchronized method
▶ 同步代码块 synchronized(Lock)
▶ 等待与唤醒 wait 和 notify
▶ 使用特殊域变量(volatile)实现线程同步
▶ 使用重入锁实现线程同步ReentrantLock
▶ 使用局部变量实现线程同步ThreadLocal
synchronized
synchronized是Java 的内置锁机制,是在JVM上的
可以同步代码块,也可以同步方法
//同步代码块
synchronized(object){
}
//同步方法
public synchronized void method() {
// do something
}
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法。
ReentrantLock
可重入锁,是一种显示锁,在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同
的基本行为和语义,并且扩展了其能力。
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
可重入: 甲获得锁后释放锁或锁失效,乙可继续获得这个锁
生产消费者模型
生产消费者模型是一个非常典型的多线程并发处理的模型,在实际的生产应用中也有非常广泛的使用。
生产消费者模型中的类–存储类
import java.util.LinkedList;
public class Store {
//创建队列
LinkedList<Integer> list = new LinkedList<Integer>();
//设置最大存储值
int max = 10;
//生产者生产 放入队尾
public void push(int n) {
synchronized(list) {
try {
if(list.size()>=max){
System.out.println("存满了");
//线程挂起
list.wait();
}else {
//队列没有存满 继续存
System.out.println("存入:"+n);
list.add(n);
//放完之后必须有 因此唤醒取的线程
list.notifyAll();
}
}catch (Exception e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
}
//消费者消费 从队头取出
public int pop() {
try {
synchronized(list){
if(list.size()<=0) {
System.out.println("队列空了。。。。");
//空了之后就不能取了 因此线程挂起
list.wait();
}else {
//从对头取出
int n = list.poll();
System.out.println("取出:"+n);
//取出了就一定不会满 因此要唤醒线程
list.notifyAll();
return n;
}
}
}catch (Exception e) {
// TODO: handle exception
}
return 0;
}
}
生产消费者模型中的类–生产者
public class Producer implements Runnable{
private Store store;
public Producer(Store store) {
// TODO 自动生成的构造函数存根
this.store = store;
}
@Override
public void run() {
// TODO 自动生成的方法存根
try {
//生产需要事件 休息100毫秒再生产
Thread.sleep(100);
//产生随机数字
store.push((int)(Math.random()*100));
}catch (Exception e) {
// TODO: handle exception
}
}
}
生产消费者模型中的类–消费者
public class Customer implements Runnable{
private Store store;
public Customer(Store store) {
// TODO 自动生成的构造函数存根
this.store = store;
}
@Override
public void run() {
// TODO 自动生成的方法存根
try {
//消费者消费需要时间 休息200毫秒
Thread.sleep(200);
//从队头取出
store.pop();
}catch (Exception e) {
// TODO: handle exception
}
}
}
测试类
package com.qingsu.pcm;
public class TestPcm {
public static void main(String[] args) {
Store store = new Store();
while(true) {
Producer producer = new Producer(store);
Customer customer = new Customer(store);
Thread t1 = new Thread(producer);
Thread t2 = new Thread(customer);
t1.start();
t2.start();
}
}
}
效果
volatile变量
volatile具有可见性、有序性,不具备原子性。
我们了解到synchronized是阻塞式同步,称为重量级锁。
而volatile是非阻塞式同步,称为轻量级锁。
被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,
从而避免出现数据脏读的现象。
线程池的概念和使用
线程的创建是比较消耗内存的,所以我们要事先创建若干个可执行的线程放进一个“池(容器)”里面,需要的时候就直接从池里面取出来不需要自己创建,使用完毕也不需要销毁而是放进“池”中,从而减少了创建和销毁对象所产生的开销。
ExecutorService:线程池接口
ExecutorService pool(池名称) = Executors.常用线程池名;
常用线程池:
newsingleThreadExecutor :单个线程的线程池,即线程池中每次只有一个线程在工作,单线程串行执行任务
newfixedThreadExecutor(n):固定数量的线程池,每提交一个任务就是一个线程,直到达到线程池的最大数量,然后在后面等待队列前面的线程执行或者销毁()。该方式一般会使线程具有一定的执行顺序
newCacheThreadExecutor:一个可缓存的线程池。当线程池超过了处理任务所需要的线程数,那么就会回收部分闲置线程(一般是闲置60s)。当有任务来时而线程不够时,线程池又会创建新的线程,当线程够时就调用池中线程。适
用于大量的耗时较少的线程任务。
newScheduleThreadExecutor:一个大小无限的线程池,该线程池多用于执行延迟任务或者固定周期的任务。
示例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestPcmTwo {
public static void main(String[] args) {
Store store = new Store();
//创建10个线程的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ExecutorService serviceTwo = Executors.newFixedThreadPool(10);
//创建可缓存的线程池
//可缓存的
//ExecutorService service = Executors.newCachedThreadPool();
//ExecutorService serviceTwo = Executors.newCachedThreadPool();
for(int i =0 ;i<11;i++) {
service.execute(new Producer(store));
}
for(int i =0 ;i<11;i++) {
serviceTwo.execute(new Customer(store));
}
//关闭线程池
service.shutdown();
serviceTwo.shutdown();
}
}
到此这篇关于一文精通Java 多线程之全方位解读的文章就介绍到这了,更多相关Java 多线程内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341