进程的介绍
进程:正在内存中运行的程序就是进程.
内存:用来运行程序,所有的程序都是在内存中运行的.
硬盘:用来持久化保存数据
线程的介绍
线程:程序的执行单元,每一个线程都可以执行一个任务
一个程序中至少有一个线程.
如果一个程序只有一个线程,那么这个程序就是单线程程序;
如果一个程序有多个线程,那么这个程序就是多线程程序;
一个线程同时只能执行一个任务.
如果是单线程程序,同时只能做一件事(单线程程序只能执行一个线程)
如果是多线程程序,同时可以做多件事(多线程程序能同时执行多个线程)
多线程程序能同时执行多个线程,这个同时并不是真正意义上的同时,真正意义上的同时是同一个时间点,多个线程一起执行.线程是由CPU调度(指挥)才会执行的,同一个时间点,一个CPU只能调度一个线程,因为CPU会在多个线程之间快速切换,且切换速度非常快,所以我们可以看成同时.
抢占式调度:线程抢夺CPU的执行权,哪个线程抢到了,哪个线程就执行,哪个线程抢到,完全是随机的.
抢占式调度只是一个通俗的说法,真正的主动权在CPU手中,是CPU看心情决定执行哪个线程.
并发和并行
并发:同一个时间,多个线程一起执行.但这个同时不是真正意义上的同时,CPU在多个线程之间快速切换,因为切换速度非常快,所以看成同时.
并行:同一个时间,多个线程一起执行.这个同时是真正意义上的同时,同一个时间点,多个线程一起执行.
并行必须要有多CPU的支持.
程序中的main线程
每一个程序都至少包含一个线程,我们写的Java程序也一样。
当程序启动时,JVM会创建一个main线程,并执行main方法。
在程序中只有一个执行线程main线程,该程序是单线程程序。
单线程程序同时只能做一件事情,如果想要同时做多件事情,可以使用多线程程序。
多线程程序的第一种实现方式
在Java中有一个类叫做Thread,这个类表示线程类,我们可以使用这个类完成多线程程序。
多线程的第一种实现方式:
1. 定义类继承Thread
2. 重写Thread中的run方法,并在run方法中定义线程要执行的任务。
3. 创建Thread子类对象。
4. 通过Thread子类对象调用start方法,启动线程,线程会执行自己的run方法。
Thread中的run方法:
void start():让线程执行,线程会执行自己的run方法。
Thread中的方法
构造方法:
Thread():空参数的构造方法。
Thread(String name):一个参数是字符串的构造方法,参数表示线程名字。
其他方法:
String getName():获取线程名字。
void setName(String name):设置线程名字。
static Thread currentThread():获取当前正在执行的线程对象。
static void sleep(long millis):线程休眠,参数是休眠的毫秒值。
多线程的第二种实现方式
实现步骤:
1. 定义类,然后实现Runnable接口。
2. 重写Runnable接口中的run方法,在run方法中定义线程要执行的任务。
3. 创建Runnable实现类对象。
4. 创建Thread线程对象,并将Runnable实现类对象作为参数传递。
5. 调用线程对象的start方法,启动线程,线程会执行对应的run方法。
推荐第二种实现方式,原因:
1. 解决了Java中类与类单继承的局限性。
2. 降低了耦合性(关联性)
3. Runnable中只有一个run方法,没有getName,sleep,setName…,功能更加纯粹,我们只需要在里面关注线程要执行的任务。
4. 更加有利于实现多线程之间数据的共享
安全性问题
Java内存模型:线程对于共享数据的访问规则.
主内存:线程共享的数据,会保存到主内存中
线程的工作内存:保存的是主内存中数据的副本.当线程要操作共享数据时,会先把主内存中的数据读取到自己的工作内存,然后再进行操作
线程无法直接操作主内存中的数据,如果线程要操作主内存中的数据,会先把主内存中的数据复制一份放到自己的工作内存中,
然后在自己的工作内存中进行操作,操作完之后会再把工作内存中的数据放回到主内存中.
各个线程的工作内存是相互不可见的.
可见性
有序性
原子性
原子性即不可分割性,如果代码不具备原子性,则可能会被插队,导致程序输出错误
volatile关键字
volatile可以解决可见性和有序性问题
volatile关键字可以保证变量对于线程的可见性.
被volatile修饰的变量,如果其某个线程对该变量进行修改并保存到主内存时,对于其他线程来说,也是可见的.
当变量被修饰为volatile时,会禁止代码重排.
volatile不能解决原子性问题
原子类
AtomicInteger介绍
AtomicInteger是整数原子类,里面支持原子性操作。
举例:如果AtomicInteger进行自增操作,自增中的很多操作步骤都是一个整体,不能被插队执行。
构造方法:
AtomicInteger():使用该构造方法创建出来的AtomicInteger对象表示整数0
AtomicInteger(int initialValue):根据指定的数字值创建AtomicInteger对象
其他方法:
public final int getAndIncrement():获取当前的值然后自增。返回的是自增前的值。
public final int incrementAndGet():先自增然后获取值。 返回的是自增后的值
int get():获取当前AtomicInteger对应的整数值
CAS机制
原子类是使用CAS机制解决原子性问题的。
CAS是通过记录旧的预期值的方式来保证原子性的.
旧的预期值:原来的值(修改前的值)
过程:
1.线程一先将主内存中的数据读取到自己的工作内存中,并记录旧的预期值
2.如果CPU的执行权被线程二抢走,线程二将主内存的数据读取到自己的工作内存中,并在自己的工作内存中进行修改,
修改后再保存到主内存中
3.线程一要将工作内存中的数据进行修改(自增操作),
4.线程一会将工作内存中的数据保存到主内存,保存前会先对比一下自己记录的旧的预期值和主内存中的数据是否一致. 如果一致,表示主内存中的数据没有被其他线程修改,可以直接将工作内存中的数据保存到主内存. 如果不一致,表示主内存中的数据已经被修改过了,那么线程一会重新读取主内存的新数据,更新旧的预期值,再继续执行步骤2…3…4…
售票案例引发的线程安全问题
synchronized
同步代码块解决线程安全问题
synchronized表示同步,可以修饰代码块,也可以修饰方法。
如果synchronized修饰代码块,那么这个代码块就是同步代码块。
同步代码块格式:
synchronized (锁对象) {
...
}
锁对象就是一个普通的Java对象,锁对象可以是任何类型的,可以是Student,Object, ArrayList
锁对象仅仅起到一个标记的作用,除此之外,就没有其他含义了。
同步代码块的作用;只有持有锁的线程才能够进入到同步代码块。
线程同步可以解决线程安全问题,会牺牲效率
同步方法解决线程安全问题
如果synchronized修饰方法,那么该方法就是同步方法,同步方法同样可以解决线程安全问题。
synchronized格式:
修饰符 synchronized 返回值类型 方法名(参数列表) {
方法体;
return 返回值;
}
同步方法相当于将整个的方法体都加了同步代码块。
同步方法也是有锁的
如果同步方法是非静态的,那么锁对象是this。
如果同步方法是静态的,那么锁对象是类名.class(字节码文件对象)
同步代码块:
优点:灵活,可以对任意代码进行同步。
缺点:语法不如同步方法简洁
同步方法:
优点:语法简洁。
缺点:不如同步代码块灵活。 是直接将整个的方法体都加了同步。
Lock接口解决线程安全问题
在JDK5的时候,提供了Lock接口,里面的方法可以手动的获取锁以及释放锁。
void lock():获取锁
void unlock():释放锁。
Lock是一个接口,如果要用,需要使用实现类,最常用的实现类是ReentrantLock
并发包
CopyOnWriteArrayList
CopyOnWriteArraySet
ConcurrentHashMap
HashMap集合不是线程安全的,如果多个线程同时操作HashMap,那么有可能会引发线程安全问题。
Hashtable集合是线程安全的,多个线程同时操作hashtable不会引发线程安全问题,效率非常低,目前基本已经淘汰了。
ConcurrentHashMap集合是线程安全的,但是效率相对Hashtable要高。 ConcurrentHashMap内部是使用的CAS机制+分段锁
CyclicBarrier
CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
CyclicBarrier构造方法:
CyclicBarrier(int count, Runnable barrierAction):当count个线程到达同步点【屏障】时,会执行barrierAction任务
其他方法:
int await():线程等待,会通知CyclicBarrier已经到达同步点【屏障】
Semaphore
Semaphore可以控制某段代码有几个线程执行。
构造方法:
Semaphore(int permits):参数表示允许几个线程共同执行某段代码。
其他方法
void acquire():获取凭证【相当于获取锁】
void release():释放凭证【相当于释放锁】
Exchanger
Exchanger是交换者,可以让线程之间交换数据。
构造方法:
Exchanger():空参数的构造方法。
其他方法:
V exchange(V x):参数表示交给其他线程的数据。返回值是其他线程发送过来的数据。调用该方法后线程会一直等待,等待其他线程发数据。
线程池
介绍
线程池就是一个容器,里面有很多线程,里面的每一个线程都可以去多次执行任务.
基本使用
线程池相关API:
Executor:接口.该接口是线程池的根接口.这个接口中提供了 执行线程任务 的方法
我们一般使用的是其子接口:ExecutorService,是Executor的子接口,也表示线程池.
里面提供了 执行任务 的方法,还提供了 管理线程池 的方法.
Executors: 线程池的工具类.里面提供了获取线程池的方法.
public static ExecutorService newFixedThreadPool(int nThreads) :返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
ExecutorService表示线程池,里面有一些方法.
public Future<?> submit(Runnable task) :获取线程池中的某一个线程对象,并执行
shutdown():销毁线程池
线程池的使用步骤:
1.调用Executors的newFixedThreadPool方法获取线程池.
2.定义一个Runnable实现类,表示线程任务.
3.通过线程池调用submit,传递Runnable接口的实现类对象,执行线程任务.
4.销毁线程池(一般不做)
Callable方式完成多线程
步骤:
1. 定义类,实现Callable接口
2. 重写Callable接口中的call方法,在call方法中定义线程要执行的任务。
3. 获取线程池。
4. 通过线程池调用submit方法,传递Callable接口的实现类对象,去执行该任务。
5. 处理结果。
线程池中提交任务的方法:
<T> Future<T> submit(Callable<T> task):提交线程任务。返回值是Future类型
Future里面封装了线程执行后的结果:
V get():获取线程执行后的结果【如果线程没有执行结束,get方法会等线程执行完毕】
死锁
死锁:线程获取不到锁对象,从而进不去同步中执行
前提:
1.必须出现同步代码块嵌套
2.必须有两个线程
3.必须有两个锁对象
同步中的线程没有出同步不会释放锁对象,没有锁的线程进不去同步
线程的六种状态
新建(NEW):刚刚创建出来但是没有运行的线程处于此状态。
运行(RUNNABLE):调用start方法启动后的线程处于运行状态。
受阻塞(BLOCKED):等待获取锁的线程处于此状态。
无限等待(WAITING):当线程调用wait()方法时,线程会处于无限等待状态【没有时间的等待】
计时等待(TIMED_WAITING):当线程调用wait(毫秒值)方法或sleep(毫秒值)时,线程会处于计时等待状态【有时间的等待】
退出(TERMINATED):当线程执行完了自己的run方法或者调用了stop方法,会进入退出状态。
wait和notify介绍
在Object中,有两种方法可以让线程等待以及唤醒线程。
void wait():线程等待,直到其他线程唤醒该线程。
void wait(long timeout):线程等待,直到其他线程唤醒该线程或者指定的时间已到。
void notify():唤醒一个线程。
void notifyAll():唤醒所有线程。
wait用于等待,notify用于唤醒等待的线程,叫做等待唤醒机制,一般用于线程间的通信。
wait和notify方法是Object中的方法,不是Thread中。
注意:wait和notify一定要写在同步代码中,要通过锁对象调用。
通过哪个锁对象调用的notify方法,唤醒的就是通过哪个锁对象调用wait等待的线程
定时器
Timer类, 表示定时器,可以只执行一次,也可以周期性的执行。
构造方法:
Timer():创建一个定时器
其他方法:
void schedule(TimerTask task, long delay):指定delay毫秒后,执行task任务,只执行一次
void schedule(TimerTask task, long delay, long period):指定delay毫秒后,执行task任务,每隔period周期性执行一次
void schedule(TimerTask task, Date time):从指定时间开始,执行task任务,只执行一次