1. 什么是进程?什么是程序?有什么区别?
程序:数据与指令的集合,程序是静态的 进程:给程序加入了时间的概念,不同的时间进程有不同的状态 进程是动态的,就代表OS中正在运行的程序 独立性,动态性,并发性
2. 什么是并行?什么是串行?什么是并发?
CPU:电脑的核心处理器,类似于“大脑”
串行:是指同一时刻一个CPU只能处理一件事,类似于单车道
并行:相对来说资源比较充足,多个CPU可以同时处理不同的多件事,类似于多车道
并发:相对来说资源比较紧缺,多个进程同时抢占公共资源,比如多个进程抢占一个CPU
3. 什么是线程?线程与进程有什么关系?
线程是OS能够进行运算调度的最小单位 一个进程可以拥有多个线程,当然,也可以只拥有一个线程,只有一个线程的进程被称作单线程程序 注意:每个线程也有自己独立的内存空间,当然也有一部分公共的空间用于保存共享的数据
在宏观上,一个CPU看似可以同时处理多件事 在微观上,一个CPU同一时刻只能处理一件事 结论:线程的执行具有随机性,我们控制不了,是由OS底层的算法来决定的
4.线程有几种状态?它们是怎么转换的?
-
新建状态:new–申请PCB,进行资源的分配
-
就绪/可运行状态:万事俱备只欠CPU,其实是将创建好的线程对象加入到就绪队列中,等待OS选中,这个选择我们是控制不了的
-
执行/运行状态:就绪队列中的线程被OS选中了,正在执行 注意:只有就绪状态才能切换成执行状态
-
阻塞状态:线程在执行中遇到了问题: 锁阻塞、休眠阻塞、等待阻塞…问题解决后再加入到就绪队列中
-
终止状态:线程成功执行完毕,释放资源
package cn.tedu.thread; /*本类用于实现多线程编程方案1:继承Thread类的方式*/ public class TestThread { public static void main(String[] args) { Thread t1 = new MyThread();/*对应的是线程的新建状态*/ Thread t2 = new MyThread(); Thread t3 = new MyThread(); Thread t4 = new MyThread(); // /*5.1测试1:自己主动调用run(),并没有多线程的效果*/ // t1.run(); // t2.run(); // t3.run(); // t4.run(); t1.start();/*对应的是线程的就绪状态*/ t2.start(); t3.start(); t4.start(); } } //1.自定义线程类,并且让这个类继承Thread类 class MyThread extends Thread{ //3.在run()完成自己的业务 //业务需求:打印10次当前正在干活的业务 @Override public void run() { for (int i = 0; i <10; i++) { System.out.println(i+getName()); } } }
package cn.tedu.thread; /*本类用于多线程编程实现方案二:实现Runnable接口*/ public class TestRunnable { public static void main(String[] args) { //4.创建自定义线程类对象 MyRunnable t = new MyRunnable(); //5.创建线程对象,并将业务对象交给线程对象 Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); Thread t4 = new Thread(t); //6.以多线程的方式启动线程 t1.start(); t2.start(); t3.start(); t4.start(); } } //1.自定义多线程类 class MyRunnable implements Runnable { //2.添加父接口中未实现的抽象方法,这个方法用来完成自己的业务 @Override public void run() { //3.完成业务:打印10次当前正在干活的线程名 for (int i = 0; i < 10; i++) { /*问题:自定义类与与父接口Runable中都没有获取名字的方法 * 所以还需要从Thread类里找 * currentThread():静态方法,获取当前正在执行的线程对象 * getName():获取当前正在执行的线程对象的名称*/ System.out.println(Thread.currentThread().getName() + i); } } }
同步:体现了排队的效果,同一时刻只能有一个线程独占资源,其他没有权利的线程排队。 坏处就是效率会降低,不过保证了安全。 异步:体现了多线程抢占资源的效果,线程间互相不等待,互相抢占资源。 坏处就是有安全隐患,效率要高一些。
synchronized同步关键字
写法
synchronized (锁对象){undefined 需要同步的代码(也就是可能出现问题的操作共享数据的多条语句); }
package cn.tedu.tickets; /*本类用于改造多线程售票案例,解决数据安全问题*/ public class TestRunnableV2 { public static void main(String[] args) { //5.创建目标业务类对象 TicketR2 target = new TicketR2(); //6.创建线程对象 Thread t1 = new Thread(target); Thread t2 = new Thread(target); Thread t3 = new Thread(target); Thread t4 = new Thread(target); //7.以多线程的方式运行 t1.start(); t2.start(); t3.start(); t4.start(); } } /*1.多线程中出现数据安全问题的原因:多线程程序+共享数据+多条语句操作共享数据*/ /*2.同步锁:相当于给容易出现问题的代码加了一把锁,包裹了所有可能会出现数据安全问题的代码 * 加锁之后,就有了同步(排队)的效果,但是加锁的话,需要考虑: * 锁的范围:不能太大,太大,干啥都得排队,也不能太小,太小,锁不住,还是会有安全隐患*/ //1.创建自定义多线程类 class TicketR2 implements Runnable { //3.定义成员变量,保存票数 int tickets = 100; //创建锁对象 Object o = new Object(); //2.实现接口中未实现的方法,run()中放着的是我们的业务 @Override public void run() { //4.通过循环结构完成业务 while (true) { /*3.同步代码块:synchronized(锁对象){会出现安全隐患的所有代码} * 同步代码块在同一时刻,同一资源只会被一个线程独享*/ /*这种写法不对,相当于每个线程进来的时候都会new一个锁对象,线程间使用的并不是同一把锁*/ //synchronized (new Object()){ //修改同步代码块的锁对象为成员变量o,因为锁对象必须唯一 synchronized (o) {//同步代码块解决的是重卖的问题 //如果票数>0就卖票 if (tickets > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } //4.1打印当前正在售票的线程名以及票数-1 System.out.println(Thread.currentThread().getName() + "=" + tickets--); } //4.2退出死循环--没票的时候就结束 if (tickets <= 0) break; } } } }
package cn.tedu.tickets; /*本类用于改造多线程售票案例,解决数据安全问题*/ public class TestThreadV2 { public static void main(String[] args) { //5.创建多个线程对象并以多线程的方式运行 TickectT2 t1 = new TickectT2(); TickectT2 t2 = new TickectT2(); TickectT2 t3 = new TickectT2(); TickectT2 t4 = new TickectT2(); t1.start(); t2.start(); t3.start(); t4.start(); } } //1.自定义多线程类 class TickectT2 extends Thread { //3.新增成员变量用来保存票数 static int tickets = 100; //static Object o = new Object(); //2.添加重写的run()来完成业务 @Override public void run() { //3.创建循环结构用来卖票 while (true) { //Ctrl+Alt+L调整代码缩进 //7.添加同步代码块,解决数据安全问题 //synchronized (new Object()) { /*static的Object的对象o这种写法也可以*/ //synchronized (o) { /*我们每通过class关键字创建一个类,就会在工作空间中生成一个唯一对应的类名.class字节码文件 * 这个类名.class对应的对象我们称之为这个类的字节码对象 * 字节码对象极其重要,是反射技术的基石,字节码对象中包含了当前类所有的关键信息 * 所以,用这样一个唯一且明确的对象作为同步代码块的锁对象,再合适不过了*/ synchronized (TickectT2.class) {/*比较标准的写法*/ if(tickets > 0){ //6.添加线程休眠,暴露问题 try { Thread.sleep(10);//让线程休眠,增加线程状态切换的频率 } catch (InterruptedException e) { e.printStackTrace(); } //4.1打印当前正在售票的线程名与票数-1 System.out.println(getName() + "=" + tickets--); } //4.2给程序设置一个出口,没有票的时候就停止卖票 if (tickets <= 0) break; } } } }
Executors 辅助创建线程池的工具类
-
newFixedThreadPool(int nThreads) 最多n个线程的线程池
-
package cn.tedu.review;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*本类用于复写多线程实现方案2:实现Runnable*/
public class TestRunnable {
public static void main(String[] args) {
//4.创建目标业务类对象---只创建一次
MyRunnable target=new MyRunnable();
/* Executors是用来辅助创建线程池的工具类
* 常用方法:
* newFixedThreadPool(int)这个方法可以创建指定数目线程的线程池对象*/
//7.使用Executors工具创建一个最多有5个线程的线程池对象ExecutorService
ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 0; i <5 ; i++) {
/*7.使用Executors工具创建一个最多有5个线程的线程池对象ExecutorService*/
pool.execute(target);
}
}
}
//1.作为目标业务类
class MyRunnable implements Runnable {
//2.实现接口中未实现的方法,里面是业务
@Override
public void run() {
//3.循环10次打印当前正在干活的线程名称
for (int i = 0; i < 10; i++) {
System.out.println(i + "=" + Thread.currentThread().getName());
}
}
}
了解
悲观锁:像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态. 悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
乐观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态. 乐观锁认为竞争不总是会发生,因此它不需要持有锁,将”比较-替换”这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
注意:查看不需要加锁
两种常见的锁
synchronized 互斥锁(悲观锁,有罪假设)
采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。 每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。
ReentrantLock 排他锁(悲观锁,有罪假设)
ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,实际上独占锁是一种相对比较保守的锁策略,在这种情况下任何“读/读”、“读/写”、“写/写”操作都不能同时发生,这在一定程度上降低了吞吐量。然而读操作之间不存在数据竞争问题,如果”读/读”操作能够以共享锁的方式进行,那会进一步提升性能。
ReentrantReadWriteLock 读写锁(乐观锁,无罪假设)
因此引入了ReentrantReadWriteLock,顾名思义,ReentrantReadWriteLock是Reentrant(可重入)Read(读)Write(写)Lock(锁),我们下面称它为读写锁。 读写锁内部又分为读锁和写锁,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。 读锁和写锁分离从而提升程序性能,读写锁主要应用于读多写少的场景。
package cn.tedu.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 本类用于改造售票案例,使用可重入读写锁
* ReentrantReadWriteLock
* */
public class TestSaleTicketsV3 {
public static void main(String[] args) {
SaleTicketsV3 target=new SaleTicketsV3();
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i <4 ; i++) {
pool.execute(target);
}
}
}
class SaleTicketsV3 implements Runnable {
//1.定义可重入读写锁对象,静态保证全局唯一
static int tickets = 100;
static ReentrantReadWriteLock s=new ReentrantReadWriteLock(true);
@Override
public void run() {
while (true) {
try {
s.writeLock().lock();
Thread.sleep(10);
if (tickets>0) {
System.out.println(Thread.currentThread().getName()+" "+tickets--);
}
if (tickets<=0){break;}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//3.finally{}中释放锁,注意一定要手动释放,防止死锁,否则就独占报错了
s.writeLock().unlock();
}
}
}
}