区别 | 进程 | 线程 |
---|---|---|
根本区别 | 作为资源分配的基本单位 | 作为CPU调度和执行的基本单位 |
开销 | 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销 | 线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小 |
所处环境 | 在操作系统中能同时运行多个任务(程序) | 在同一应用程序中有多个顺序流同时执行 |
分配内存 | 系统在运行的时候会为每个进程分配不同的内存区域 | 除了CPU外,不会为线程分配内存(线程所使用的资源时它所属的进程的资源),线程组只能共享资源 |
包含关系 | 没有线程的进程是可以被看做单线程的,如果一个进程内拥有多个线程,则执行过程中不是一条线的,而是多条线(线程)共同完成的 | 线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程 |
很多多线程是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器。如果是模拟出来的多线程,即一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉
在程序运行时,即使没有自己创建线程,后台也会存在多个线程,如gc线程、主线程
main()称之为主线程,为系统的入口点,用于执行整个程序
在一个进程中,如果开辟了多个线程,线程的运行是由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为干预的
对同一份资源操作时,会存在资源抢夺问题,需要加入并发控制
线程会带来额外的开销,如cpu调度时间、并发控制开销
每个线程在自己的工作内存交互、加载和存储,主内存控制不当会造成数据不一致
进程:正在运行的程序
线程:是进程中的单个顺序控制流,是一条执行路径
创建线程的三种方法
1.继承Thread类,Thread是一个线程类
2.实现Runnable接口
public class MyThread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class MainThread1 {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
// 这里是把myThread1作为1个公共的资源
new Thread(myThread1, "线程A").start();
new Thread(myThread1, "线程B").start();
new Thread(myThread1, "线程C").start();
}
}
相比继承Thread类,实现Runnable接口的好处:
3.实现Callable接口
如果想要执行线程,就必须调用start()方法,将线程加入到调度器中,此时线程不一定立即执行,具体是有系统安排调度分配执行,直接调用run()方法不是开启多线程,而是普通方法
public class Exe1 {
public static void main(String[] args) {
ReturnOneParam lambda1 = a -> doubleNum(a);
System.out.println(lambda1.method(3));
//lambda2 引用了已经实现的 doubleNum 方法
ReturnOneParam lambda2 = Exe1::doubleNum;
System.out.println(lambda2.method(3));
Exe1 exe = new Exe1();
//lambda4 引用了已经实现的 addTwo 方法
ReturnOneParam lambda4 = exe::addTwo;
System.out.println(lambda4.method(2));
}
/**
* 要求
* 1.参数数量和类型要与接口中定义的一致
* 2.返回值类型要与接口中定义的一致
*/
public static int doubleNum(int a) {
return a * 2;
}
public int addTwo(int a) {
return a + 2;
}
}
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
void join() | 等待这个线程死亡(如果某个线程调用了这个方法,那么其它的线程必须等待这个线程执行完毕才有机会执行) |
void setDaemon(boolean on) | 将此线程标记位守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 |
静态方法
非静态方法
setName(String name):将此线程的名称更改为name,默认名称为Thread-i, i = 0, 1, 2…
getName():获取当前正在执行的线程的名称
setPriority(int newPriority):更改此线程的优先级
getPriority():返回此线程的优先级
线程优先级的设定建议在start()调用前
run():封装线程执行的代码,如果直接调用run()方法,相当于调用一个普通方法
start():启动线程,然后由JVM调用此线程的run()方法
void join():插队,等待这个线程死亡(如果某个线程调用了这个方法,那么其它的线程必须等待这个线程执行完毕才有机会执行)
myThread1.start();
try {
myThread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
myThread2.start();
myThread3.start();
1.线程停止:stop()、destroy()
class MyThread implements Runnable {
/**
* 线程类中定义线程体使用的标识
*/
private boolean flag = true;
@Override
public void run() {
/**
* 2.线程体使用该标识
*/
while (true) {
System.out.println("线程正在运行");
}
}
/**
* 3.对外提供方法改变标识
*/
public void stop() {
this.flag = false;
}
}
2.static void sleep(long millis): 使当前正在执行的线程停留(暂停执行)指定的毫秒数
3.礼让:static void yield()
4.插队:join()
5.等待:wait()
6.Thread.activeCount():当前正在活动的线程数
7.boolean isAlive():判断线程是否还活着,即线程是否还未终止
8.setName(): 给线程取一个名字
9.getName(): 获取线程名字
10.currentThread():获取当前正在运行的线程对象
public class SellTicket implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");
ticket--;
}
}
}
}
public class Main {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
new Thread(sellTicket, "窗口A").start();
new Thread(sellTicket, "窗口B").start();
new Thread(sellTicket, "窗口C").start();
}
}
public class SellTicket implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");
ticket--;
}
}
}
}
public class Main {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
new Thread(sellTicket, "窗口A").start();
new Thread(sellTicket, "窗口B").start();
new Thread(sellTicket, "窗口C").start();
}
}
* 这样会出现票的重复卖出、卖出负数票的情况
使用同步代码块解决上述出现的线程问题
由于我们上述的案例满足了这三个要求,所以会出现安全问题
只有三条都满足了,才会出现安全问题
如何解决多线程安全问题?
如何实现?
同步代码块:
同步的好处和弊端:
同步方法:就是把synchronized关键字加到方法上
同步静态方法:就是把synchronized关键字加到静态方法上
线程安全的类:StringBuffer、Vector、Hashtable
一般来说:多线程中使用StringBuffer,但是Vector和Hashtable在多线程中也不使用,
优点:
缺点:
synchronized 方法控制对“成员变量|类变量”对象的访问:每个对象对应一把锁,每个synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。
由于我们可以通过 private关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和 synchronized块。
同步块可以粒度更小的锁定资源
可重入锁:如果某个线程试图获取一个已经由它自己持有的锁时,那么这个请求会立即成功,并且会将这个锁的计数值加1,而当线程退出同步代码块时,计数器会递减,并且当计数值等于0时,机会释放锁。如果没有可重入锁的支持,在第二次企图获得锁时将会进入死锁状态
可重入锁:某个线程已经获得某个锁,可以再次获取锁而不会出现死锁,而其他的线程是不可以的
synchronized 和 ReentrantLock 都是可重入锁。
可重入锁的意义之一在于防止死锁。
可重入锁的实现原理:
自定义不可重入锁:
public class LockTest {
private MyLock myLock = new MyLock();
public void a() throws InterruptedException {
myLock.lock(); // (isLocked = false) -> myLock.lock() --> isLocked = true
doSomething();// isLocked = true --> myLock.lock() --> wait() 于是本线程就一直处于阻塞状态,无法解除
myLock.unlock();
}
public void doSomething() throws InterruptedException {
myLock.lock();
System.out.println("测试。。。。。");
myLock.unlock();
}
public static void main(String[] args) throws InterruptedException {
LockTest lockTest = new LockTest();
lockTest.a();
}
}
/**
* 不可重入锁
*/
class MyLock{
/**
* 是否被持有
* true:被持有 false:未被持有
*/
private boolean isLocked = false;
/**
* 使用锁
*/
public synchronized void lock() throws InterruptedException {
while (isLocked) {
wait();
}
isLocked = true;
}
/**
* 释放锁
*/
public synchronized void unlock() {
isLocked = false;
notify();
}
}
public class LockTest1 {
private MyLock1 myLock1 = new MyLock1();
public void a() throws InterruptedException {
myLock1.lock(); // (isLocked = false) -> myLock.lock() --> isLocked = true
doSomething();// isLocked = true --> myLock.lock() --> wait() 于是本线程就一直处于阻塞状态,无法解除
myLock1.unlock();
}
public void doSomething() throws InterruptedException {
myLock1.lock();
System.out.println("测试。。。。。");
myLock1.unlock();
}
public static void main(String[] args) throws InterruptedException {
LockTest1 lockTest1 = new LockTest1();
lockTest1.a();
}
}
/**
* 可重入锁
*/
class MyLock1{
/**
* 是否被持有
* true:被持有 false:未被持有
*/
private boolean isLocked = false;
/**
* 记录这个锁是否是当前线程中的,如果是,那就不等待
*/
private Thread lockedBy = null;
/**
* 锁的计数器
*/
private int holdCount = 0;
/**
* 使用锁
*/
public synchronized void lock() throws InterruptedException {
Thread thread = Thread.currentThread();
while (isLocked && lockedBy != thread) {
wait();
}
isLocked = true;
lockedBy = thread;
holdCount++;
}
/**
* 释放锁
*/
public synchronized void unlock() {
if (Thread.currentThread() == lockedBy) {
holdCount--;
if (holdCount == 0) {
isLocked = false;
notify();
lockedBy = null;
}
}
}
}
根据锁是否沿用:
悲观锁:synchronized是独占锁即悲观锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁
乐观锁:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止
Compare and Swap:比较并交换
乐观锁的实现:
CAS是硬件级别的操作,(利用CPU的CAS指令,同时借助JNI来完成的非阻塞算法),效率要比加锁操作高
CAS是一组原子操作,不会被外部打断
但是存在AVA的问题:如果变量Version初次读取的时候也是A,并且在准备赋值的时候检查到它仍然是A,
那能说明它的值没有被其它线程修改过了吗?如果子啊这段期间曾经被改为B,然后又改回A,那CAS操作就会误认为它从来没有被修改过
public class SellTicket implements Runnable{
private int ticket = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 通过lock获得锁
lock.lock();
try {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票");
ticket--;
}
}finally {
// 通过lock释放锁
lock.unlock();
}
}
}
}
public class Main {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
new Thread(sellTicket, "窗口A").start();
new Thread(sellTicket, "窗口B").start();
new Thread(sellTicket, "窗口C").start();
}
}
生产者消费者模式是一个十分经典的多线程协作的模式
所谓生产者消费者问题,实际上主要包含了两类线程:
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
为了体现生产和消费过程中的等待和唤醒,Java就提供了几个方法供我们使用功能。
这几个方法在Object类中,Object类的等待和唤醒方法
方法名 | 说明 |
---|---|
void wait() | 导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法 |
void notify() | 唤醒正在等待对象监视的单个线程 |
void notifyAll() | 唤醒正在等待对象监视的所有线程 |
生产者消费者案例中包含的类:
生产者消费者问题是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖、互为条件
线程通信
/**
* 协作模型:生产者消费者实现方式-管程法
* 角色:
* 多线程生产者
* 多线程消费者
* 缓冲区
* 数据
* @author NiuQun
* @date 2021/9/14
*/
public class Main {
public static void main(String[] args) {
Container container = new Container();
new Producer(container).start();
new Consumer(container).start();
}
}
/**
* 多线程生产者
*/
class Producer extends Thread {
private Container container;
private int produceDadaCount = 0;
public Producer(Container container) {
this.container = container;
}
/**
* 持续生产数据
*/
@Override
public void run() {
while (true) {
synchronized (container) {
container.push(new Data(++produceDadaCount));
System.out.println("正在生产第" + produceDadaCount + "个数据");
}
}
}
/* 生产有限个数据
@Override
public void run() {
// 生产
for (int i = 0; i < 100; i++) {
container.push(new Data(i));
System.out.println("生产第" + i + "个数据");
}
}
*/
}
/**
* 多线程消费者
*/
class Consumer extends Thread {
private Container container;
public Consumer(Container container) {
this.container = container;
}
/**
* 持续消费数据
*/
@Override
public void run() {
while (true) {
synchronized (container) {
System.out.println("正在消费生产的第" + container.pop().getId() + "个数据");
}
}
}
/* 消费有限个数据
@Override
public void run() {
// 消费有限个数据
for (int i = 0; i < 1000; i++) {
System.out.println("消费第" + container.pop().getId() + "个数据");
}
}
*/
}
/**
* 缓冲区
*/
class Container {
/**
* 存储数据的容器
*/
private Data[] dataArr = new Data[10];
/**
* count是计数器,count指向下一个可以存储数据的位置
*/
private int count = 0;
/**
* 存储数据
* 容器存在空间时,才能生产
* @param data
*/
public void push(Data data) {
if (count == dataArr.length) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 先在当前位置存,然后索引先后移动一位,所以count指向的是下一个可以存储数据的位置
dataArr[count++] = data;
notifyAll(); // 存在生产了,可以唤醒消费者
}
/**
* 获取数据
* 只有当容器中存在数据时才可以消费
* 容器中没有数据时线程只能等待
*/
public Data pop() {
if (count == 0) {
try {
wait(); // 此时线程阻塞 ,当生产者通知消费时,才能解除阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 由于count指向的是下一个可以存储数据的位置,所以count应当先减1,再取值
Data data = dataArr[--count];
notifyAll();// 存在空间了,可以通知生产者生产
return data;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
/**
* 数据
*/
class Data {
/**
* id用于统计生产的数据总量
*/
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Data(int id) {
this.id = id;
}
}
/**
* 生产者
* 消费者
* 资源
* @author NiuQun
* @date 2021/9/15
*/
public class Main {
public static void main(String[] args) {
Data data = new Data();
new Producer(data).start();
new Consumer(data).start();
}
}
class Producer extends Thread{
private Data data;
public Producer(Data data) {
this.data = data;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
data.setResource("偶数");
}else {
data.setResource("奇数");
}
}
}
}
class Consumer extends Thread{
private Data data;
public Consumer(Data data) {
this.data = data;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
data.getResource();
}
}
}
class Data {
/**
* resource代表存储的资源
*/
private String resource;
/**
* signal为true表示生产者生产,消费者等待
* signal为false表示生产者等待,消费者消费
*/
private boolean signal = true;
/**
* 获取资源
* @return
*/
public synchronized String getResource() {
// 1.(共享区域无资源时)消费者无法获取数据时
// 生产者要生产资源,消费者等待
if (signal) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 2.(共享区域有资源时)消费者可以消费
System.out.println("消费者消费了" + resource);
notifyAll();
signal = true;
return resource;
}
/**
* 存储资源
* @param resource
*/
public synchronized void setResource(String resource) {
// 1.(共享区域有资源时)生产者无法生产数据
// 消费者要消费数据,生产者等待
if (!signal) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 当signal = true时,生产者可以生产
// 2.(共享区域无资源时)生产者可以生产数据
this.resource = resource;
System.out.println("生产者生产了" + resource);
notifyAll();
signal = false;
}
}
方法:
方法名 | 作用 |
---|---|
void wait() | 导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法,与sleep()不同,wait()会释放锁 |
void wait(long timeout) | 指定等待的毫秒数 |
void notify() | 唤醒一个处于等待状态的线程 |
void notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度 |
以上四个方法军事java.lang.Object类中的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常
wait()和sleep():
/**
* 任务调度:Timer类和TimerTask抽象类
* @author NiuQun
* @date 2021/9/15
*/
public class MyTimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
// 执行安排,1秒后执行线程new MyTask()线程,之后每隔200毫秒执行一次
// timer.schedule(new MyTask(), 1000);
// 执行安排,1秒后执行new MyTask()线程,之后每隔200毫秒执行一次
// timer.schedule(new MyTask(), 1000, 200);
// 执行安排, 指定时间第一次执行new MyTask()线程,之后每隔200毫秒执行一次
Calendar cal = new GregorianCalendar(2021, 12, 21, 21, 21, 21);
timer.schedule(new MyTask(), new Date(5000), 200);
}
}
/**
* 任务类
*/
class MyTask extends TimerTask {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("放空大脑,休息一会儿");
}
System.out.println("end............");
}
}
public class ThreadLocalTest {
/**
* 更改初始值方式1
* 采用匿名内部类的方式
*/
private static ThreadLocal threadLocal = new ThreadLocal(){
@Override
protected Integer initialValue() {
return 200;
}
};
/**
* 更改初始值方式2
* jdk1.8之后提供的:Lambda表达式
*/
private static ThreadLocal threadLocal1 = ThreadLocal.withInitial(()->300);
public static void main(String[] args) {
// 300
System.out.println(Thread.currentThread().getName() + "--->" + threadLocal.get());
threadLocal.set(100);
// 400
System.out.println(Thread.currentThread().getName() + "--->" + threadLocal.get());
new Thread(new MyRun()).start();
}
public static class MyRun implements Runnable {
@Override
public void run() {
// threadLocal.set(400);
// 400
System.out.println(Thread.currentThread().getName() + "--->" + threadLocal.get()); // 200
}
}
}
public class InheritableThreadLocalTest {
private static ThreadLocal threadLocal = new InheritableThreadLocal();
public static void main(String[] args) {
// null
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
threadLocal.set(100);
// 100
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
new Thread(()->{
// 100
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
threadLocal.set(200);
// 200
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
}).start();
}
}