多线程编程
- 进程 : 正在运行的程序,当我们的一个程序进入内存运行时,也就成了一个进程。
进程是出于运行过程中的程序,具有一定的独立功能
进程是系统进行资源分配和调度的一个独立的单位
进程是正在运行的程序,进程负责给程序分配内存
一、创建线程的几种方法
Thread<无返回值>
- 继承Thread类
- 重写run方法
- 使用start方法启动线程
class Demo extends Thread{
@Override
public void run(){
System.out.println("这是创建的线程任务");
}
public satic void main(String[] args){
//开启线程任务
Demo demo = new Demo();
demo.start();
}
}
Thread常用方法
- Thread.currentThread().getName() 获取线程名称
- getName() 返回线程名称
- getId() 返回该线程的标识符
- getPriority() 返回优先级
- getState() 返回线程状态
- interrupt() 中断线程
- isInterrupted() 测试线程是否已经中断。
- isAlive() 测试线程是否处于活动状态。
- isDaemon() 测试该线程是否为守护线程。
- join() 等待该线程终止。
- setDaemon(boolean) 将该线程标记为守护线程或用户线程。
- setName(java.lang.String) 改变线程名称,使之与参数
name
相同。 - setPriority(int) 更改线程的优先级。
- sleep(long) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
- yield() 暂停当前正在执行的线程对象,并执行其他线程。暗示进程放弃依次资源抢占
Runnable<无返回值>
- 实现Runnable接口
- 实现run方法
- 通过new Thread(实现Runnable接口的对象).start()启动线程
实现接口
class Demo01 implement Runnable{
public void run(){
System.out.println("这是创建的线程任务");
}
public satic void main(String[] args){
//开启线程任务
Demo01 demo = new Demo01();
new Thread(demo).start();
}
}
匿名内部类
class Dmeo02{
public static void main(String[] args){
new Thread(()->{
System.out.println("这是创建的线程任务");
}).start();
}
}
Callable <有返回值>
通过继承Callable接口
实现call方法
class CallableDemo implements Callable<Integer>{
@Override
public Integer call() throws Exception{
int sum = 0 ;
for(int i = 0 ; i < 100 ; i ++){
sum += i;
System.out.println("使用Callable接口创建子线程"+i);
}
return sum ;
}
}
FutureTask
我们实现Callable 之后,我们发现这个类是没有start方法 的
此时,我们就需要借助FutureTask这个类
这个类它实现了Future接口
而Future接口继承了我们的Runnable 和Callable 接口
此时,我们就可以通过如下方法,创建这个线程(多态的原理)
class Test{
public static void main(String[] args){
CallableDemo demo = new CallableDemo();
FutureTask ft = new FutureTask(demo);
new Tread(ft).start();
//我们可以通过get方法来获取这个返回值
try{
Integer sum = (Integer)ft.get();
System.out.println("Callable子线程返回:"+sum);
}catch(Expection e){
e.printStacjTrace();
}
}
}
二、线程安全问题
Thread 和 Runnable 的区别
- 对于Thread
class Demo extends Thread {
private int count ;
@Override
public void run(){
for(int i = 0 ; i < 10 ; i ++){
count ++ ;
}
System.out.println(count);
}
public static void main(String[] args){
Demo demo = new Demo();
Demo demo1 = new Demo();
demo.start();
demo1.start();
}
}
/*执行输出:
10
10
*/
- 对于Runnable
class Demo implements Runnable{
private int count ;
@Override
public void run(){
for(int i = 0 ; i < 10 ; i ++){
count ++ ;
}
System.out.println(count);
}
public static void main(String[] args){
Demo demo = new Demo();
Demo demo1 = new Demo();
new Thread(demo).start();
new Thread(demo1).start();
}
}
/*执行输出:
7
7
多次执行发现,没次结果都不大相同
*/
通过对上方的两个程序结果对比
我们发现
继承Thread 的线程对象,不能够共享数据
实现Runnable 的线程对象,使用不同的Thread对象,同时执行一个实现Runnable接口的对象,则会出现共享数据的现象,可能会出现线程安全问题.
三、如何解决线程安全问题
通过加锁,解决
synchronized关键字 – 锁
- java提供的同步锁,synchronized有三种使用方式
- 必须使用一个对象作为钥匙
- 锁加的范围越小,效率越高
三种使用方法
- 同步块
synchronized(key){
//同步代码,key必须是一个对象
}
class Demo implements Runnable{
private int count ;
Object obj = new Object();
@Override
public void run(){
for(int i = 0 ; i < 10 ; i ++){
synchronized(obj){
count ++ ;
}
}
System.out.println(count);
}
public static void main(String[] args){
Demo demo = new Demo();
Demo demo1 = new Demo();
new Thread(demo).start();
new Thread(demo1).start();
}
}
/*运行结果
10
20
*/
- 将方法加锁
如果某个方法都是可能出现线程安全问题
则建议加在方法上
该方法在充当锁的钥匙
class Demo implements Runnable{
private int count ;
Object obj = new Object();
class Demo implements Runnable{
private int count ;
@Override
public synchronized void run(){
for(int i = 0 ; i < 10 ; i ++){
count ++ ;
}
System.out.println(count);
}
public static void main(String[] args){
Demo demo = new Demo();
Demo demo1 = new Demo();
new Thread(demo).start();
new Thread(demo1).start();
}
}
/*运行结果
10
20
*/
- 将静态方法加锁
此时充当钥匙的是,静态方法所在的静态类
类.class 可以拿到当前类的字节码对象
效率
synchronized 同步锁在jdk7之前,默认调用系统锁(重量级锁)效率慢
基于如上原因,jdk5,JUC包诞生,提供了一个Lock锁
Lock接口
lock锁,是jdk5.0提供的锁,是一个可重入锁(ReentrantLock)
可以充当公平锁,也可以充当不公平锁
一旦加锁,最后必须释放锁,否则会出现死锁现象
将释放锁的代码写入finally中
常用的实现子类 ReentrantLock
class Demo implements Runnable{
private int count ;
private Lock lock = new ReentrantLock();
@Override
public void run(){
for(int i = 0 ; i < 10 ; i ++){
try{
//手动上锁
lock.lock();
count ++ ;
//必须写入try中
}finally{
//手动解锁
lock.unlock();
}
}
System.out.println(count);
}
public static void main(String[] args){
Demo demo = new Demo();
Demo demo1 = new Demo();
new Thread(demo).start();
new Thread(demo1).start();
}
}
/*运行结果
10
20
*/
synchronized锁升级
synchronized:偏向锁(无锁),自旋锁(CAS),重量级锁
乐观锁和悲观锁:
- synchronized 和 Lock 典型的悲观锁
死锁
两个线程之间,进行资源竞争时,彼此之间拿着对方需要的资源,但是因为线程的竞争性(请求保存),形成一种相互等待对方释放资源的现象,这种现象就叫死锁
死锁的必要条件
- 互斥
- 请求保存
- 环路等待
- 不可剥夺性
死锁只能尽量避免,不能完全解决
单例模式
我们在编程中,会遇到多个地方使用同一个对象的情况
而为了避免对象的重复创建
我们有两种单例模式: 饿汉式,懒汉式
饿汉式
- 它是将一个已经创建好的静态对象,通过一个访问器,向外提供
class Demo{
//已经创建好的静态对象
private static Demo demo = new Demo();
//私有化我们的构造函数
private Demo(){}
//提供一个访问器
public Demo newDemo(){
return this.demo;
}
}
此时我们在外部就可以通过访问器,来访问这个已经创建好的对象
懒汉式
- 和饿汉式一样,都是提供一个访问器,返回内部创建好的对象
- 不同的是,饿汉式他是已经创建好的对象,而懒汉式则是在第一次调用构造函数时才创建对象
class Demo{
//定义一个对象变量
private static Demo demo = null;
//私有化我们的构造函数
private Demo(){}
//提供一个访问器
public Demo newDemo(){
//判断是否是第一次访问
if(demo==null){
demo = new Demo();
}
return this.demo;
}
}
双重检查锁
-
对于懒汉式来说,在单线程模式下,是没有问题的,但是在多线程下他就会出现很大的问题,
-
例如说,a通过访问器获取对象,此时demo==null,当程序正要创建对象时,b抢占了资源,此时demo依旧为空
-
为了避免这种情况,我们可以使用双重检查锁
class Demo{
//定义一个对象变量
private static Demo demo = null;
//私有化我们的构造函数
private Demo(){}
//提供一个访问器
public Demo newDemo(){
//判断是否是第一次访问
if(demo==null){
//加锁
synchronized(Demo.class){
//双重判断
if(demo==null){
demo = new Demo();
}
}
}
return this.demo;
}
}
双重检查锁,并不是一定有效的
在编译过程中,代码优化可能会改变我们的代码顺序
因此,我们必须禁止代码优化
volatile关键字
- 被volatile修饰的变量,具有线程 可见性
- 被volatile修饰的变量,是禁止指令重排序的
因此,正确的双重检查锁的形式应该如下
class Demo{
//定义一个对象变量
private static volatile Demo demo = null;
//私有化我们的构造函数
private Demo(){}
//提供一个访问器
public Demo newDemo(){
//判断是否是第一次访问
if(demo==null){
//加锁
synchronized(Demo.class){
//双重判断
if(demo==null){
demo = new Demo();
}
}
}
return this.demo;
}
}
四、线程的生命周期
五、同步问题(协同步调)和 唤醒机制
同步问题实现方案
- 可以使用lock锁来实现
- 使用同步锁配合唤醒机制
唤醒机制
- notify 方法 唤醒处于wait状态的对象
- notifyAll 方法 唤醒所有处于wait状态的对象
- wait 方法 进入到等待状态
唤醒机制必须配合 同步锁
- 必须有一个共有对象
- 该共有对象作为同步锁的钥匙
生产者和消费者模式(同步唤醒机制的实现):
-
一个简单的供需平衡问题
-
生产者
package ThreadDemo;
import java.util.Random;
/**
* @author : YWJ
* @date : 2022/11/14 : 20:46
*/
public class Product extends Thread{
private String[] foods ;
private Disk disk ;
private Random random ;
public Product(Disk disk) {
this.foods = new String[]{"牛肉面","兰州拉面","削筋面","担担面","重庆小面","浆水面","油泼面"};
this.disk = disk ;
this.random = new Random();
}
@Override
public void run() {
makeFood();
}
private void makeFood() {
synchronized (this.disk) {
for (int i = 0; i < 10; i++) {
if (this.disk.isFull()) {
//创造食物
String food = this.foods[this.random.nextInt(this.foods.length)];
System.out.println("生产了一个食物:" + food);
this.disk.setFood(food);
this.disk.setFull(false);
//唤醒消费者
this.disk.notify();
//让生产者进入等待
try {
this.disk.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//让生产者进入等待
try {
this.disk.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
- 消费者
package ThreadDemo;
/**
* @author : YWJ
* @date : 2022/11/14 : 20:46
*/
public class Customer extends Thread {
private Disk disk ;
public Customer(Disk disk,String name) {
super(name);
this.disk = disk;
}
@Override
public void run() {
synchronized (this.disk) {
while (true) {
if (!this.disk.isFull()) {
String food = this.disk.getFood();
System.out.println(currentThread().getName() + "消费了一次 :" + food);
this.disk.setFull(true);
this.disk.notify();
try {
this.disk.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
try {
this.disk.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
- 共有属性盘子
package ThreadDemo;
/**
* @author : YWJ
* @date : 2022/11/14 : 20:44
*/
public class Disk {
public String getFood() {
return food;
}
public void setFood(String food) {
this.food = food;
}
public boolean isFull() {
return full;
}
public void setFull(boolean full) {
this.full = full;
}
private String food;
private boolean full = true;
}
- 代码测试
package ThreadDemo;
/**
* @author : YWJ
* @date : 2022/11/14 : 21:14
*/
public class TestCP {
public static void main(String[] args) {
Disk disk = new Disk();
Product product = new Product(disk);
Customer customer = new Customer(disk,"张三");
//将消费者设置成守护线程
customer.setDaemon(true);
product.start();
customer.start();
}
}
- 执行结果
生产了一个食物:削筋面
张三消费了一次 :削筋面
生产了一个食物:重庆小面
张三消费了一次 :重庆小面
生产了一个食物:削筋面
张三消费了一次 :削筋面
生产了一个食物:削筋面
张三消费了一次 :削筋面
生产了一个食物:油泼面
张三消费了一次 :油泼面
生产了一个食物:担担面
张三消费了一次 :担担面
生产了一个食物:担担面
张三消费了一次 :担担面
生产了一个食物:削筋面
张三消费了一次 :削筋面
生产了一个食物:削筋面
张三消费了一次 :削筋面
生产了一个食物:担担面
张三消费了一次 :担担面