多线程和锁机制

本文详细介绍了进程与线程的概念及区别,包括多线程的实现方式、数据安全问题及解决方案,探讨了线程池、定时器等高级主题,并解析了Java内存模型中的可见性问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

进程与线程

进程的概念: 线程依赖于进程,进程是系统进行资源分配和调用的独立单位,每个进程都有他自己的内存空间和系统资源。单进程计算机同一时间点只能做一件事情。多核CPU支持多进程,同一时间可以执行多个任务。
线程的概念: 同一进程内又可以执行多个任务,每一个任务可以称之为一个线程。线程,是程序执行单位,执行路径。是程序使用CPU的基本单位。单线程,程序只有一条执行路径。多线程,程序有多条执行路径。
进程与线程的区别: 打开游戏是一个进程,打开音乐播放器是一个进程。点击播放是一个线程,下载音乐是一个线程。
多线程的意义: 多线程的存在,不是提高程序的执行速度,而是为了提高应用程序的使用率。程序的执行本质上都是在抢占CPU的资源,CPU的执行权。当多个进程抢占CPU资源时,哪个进程如果执行路径较多,则会有更高的几率抢到CPU的执行权,所以线程的执行具有随机性
并行与并发的区别: 并行是逻辑上同时发生,指在某一时间段内同时运行多个程序。并发是物理上同时发生,指在某一时间点同时运行多个程序。

一、多线程程序的实现方式

线程是依赖进程存在的,要想实现多线程就应该先创建出来一个进程。而进程是由系统创建的,所以要通过调用系统功能创建一个进程。Java不能直接调用系统功能,所以没有办法直接实现多线程程序。所以Java通过调用C/C++写好的程序来实现多线程程序。由C/C++去调用系统功能创建进程,然后由Java去调用,可以得到一些类供我们使用,就可以实现多线程程序了。

(一) 实现方式1

  1. 创建Thread类的子类
  2. 重写Thread类中的run();方法(run();方法中封装的是必须被线程执行的代码,run方法中一般书写比较耗时的代码)
  3. start();开启线程
    在这里插入图片描述

(二) 实现方式2

实现Runnable接口,优点是扩展性强,在实现一个接口的同时,还能再去继承其他类。

  1. 定义一个类并实现Runnable接口
  2. 重写接口中的run()方法
  3. 创建线程对象,把Runnable接口中子类对象,作为参数传递进来
  4. 开启线程
public class MyThread implements Runnable {

	@Override
	public void run() {
		
		for(int x = 0 ; x < 100 ; x++){
			System.out.println(Thread.currentThread().getName() + "---" + x);
		}
		
	}

}
--------------------------------------------------------------------------------------------------------
public static void main(String[] args) {
		
		// 创建对象
		MyThread mt1 = new MyThread() ;
		MyThread mt2 = new MyThread() ;
		
		// 创建Thread对象
		Thread t1 = new Thread(mt1 , "张三") ;
		Thread t2 = new Thread(mt2 , "李四") ;
		
		// setName给线程设置名称
//		t1.setName("张三") ;
//		t2.setName("李四") ;
		
		// 启动
		t1.start() ;
		t2.start() ;
		
	}

二、多线程的数据安全问题

(一) 线程安全问题出现的条件

  • 是否是多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

(二) 同步代码块的方式解决线程安全问题

synchronized(对象){//不能在括号了直接new 对象 new 了 就没效果,要定义为静态成员变量,才能被所有线程共享
			要被同步的代码 ;
 		}
  • 同步的好处:同步的出现解决了多线程的安全问题。
  • 同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
  • 同步代码块的锁对象:任意一个对象
  • 同步方法的锁对象:是this
  • 静态同步方法的锁对象:就是当前类对应的字节码文件对象

(三) Lock锁解决线程安全问题

由于同步代码块不能直观的看到在哪里加了锁,在哪里释放了锁。为了更清晰的表达如何加锁和释放锁,JDK1.5以后提供了新的锁对象Lock

void lock();//开锁
void unlock();//关锁

---------------------------------------------------------------
public class MyThread implements Runnable {
   
   // 定义票数
   private static int tickets = 100 ;
   
   // 创建Lock锁对象
   private static final Lock lock = new ReentrantLock() ;

   @Override
   public void run() {
   	
   	while(true){
   		
   		// 添加锁
   		lock.lock() ;
   		
   		if(tickets > 0){
   			
   			try {
   				Thread.sleep(100);
   			} catch (InterruptedException e) {
   				e.printStackTrace();
   			}
   			
   			System.out.println(Thread.currentThread().getName() + "正在出售" + (tickets--) + "张票");
   			
   		}
   		// 释放锁
   		lock.unlock() ;
   	}
   }
}
--------------------------------------------------------
public class MyThreadDemo {
   
   public static void main(String[] args) {
   	
   	// 创建MyThread类的对象
   	MyThread mt = new MyThread() ;
   	
   	// 创建3个线程对象
   	Thread t1 = new Thread(mt , "窗口1") ;
   	Thread t2 = new Thread(mt , "窗口2") ;
   	Thread t3 = new Thread(mt , "窗口3") ;
   	
   	// 启动线程
   	t1.start() ;
   	t2.start() ;
   	t3.start() ;
   }
}

(四) 死锁现象

同步嵌套,将导致死锁。

public interface MyLock {
   
   public static final Object objA = new Object() ;
   public static final Object objB = new Object() ;

}

----------------------------------
public class MyThread extends Thread {
   
   private boolean flag  ;
   
   public MyThread(boolean flag){
   	this.flag = flag ;
   }
   
   @Override
   public void run() {
   
   	if(flag){
   		
   		synchronized (MyLock.objA) {
   			System.out.println("true......objA...................");
   			//嵌套同步代码块
   			synchronized (MyLock.objB) {
   				System.out.println("true.......objB.............");
   			}
   		}
   		
   	}else {
   		
   		synchronized (MyLock.objB) {
   			System.out.println("false......objB...................");
   			//嵌套同步代码块
   			synchronized (MyLock.objA) {
   				System.out.println("false.......objA.............");
   			}
   		}
   	}
   
   }
}
-----------------------------------------
public class DeadLockDemo {
   
   public static void main(String[] args) {
   	
   	// 创建两个线程对象
   	MyThread mt1 = new MyThread(true) ;
   	MyThread mt2 = new MyThread(false) ;
   	
   	// 启动线程
   	mt1.start() ;
   	mt2.start() ;
   }
}

(五) 等待唤醒机制

线程的等待唤醒:Object类
void wait (); 在其他线程调用此对象的 notify () 方法或 notifyAll () 方法前,导致当前线程等待。
void notify();唤醒在此对象监视器上的等待的单个线程。
void notifyAll();唤醒在此对象监视器上等待的所有线程。

生产线程:如果没有资源资源我就生产,有了资源我就等待,通知消费线程来消费
消费线程:有了资源我就消费,没有资源我就等着,你通知生产线程生产

sleep()和wait()方法:
相同点:这两个方法都能使线程处于阻塞状态。
区别:sleep()方法必须要一个时间,wait()方法可以要时间也可以不传时间。sleep()线程休眠后不释放锁,wait()一旦等待就要释放锁。

(六)线程的生命周期

在这里插入图片描述

三、线程池

程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而线程池作为一个装有线程对象的容器,可以帮我们管理线程对象。线程池会预先创建一些线程对象,放在线程池内,当有任务需要执行时,就可以让线程池中的线程去执行任务,当任务完成后,线程可回收。
利用Executors工厂来产生线程池:

public static ExecutorService newCachedThreadPool(): //根据任务的数量来创建线程对应的线程个数(无边界线程池)	
public static ExecutorService newFixedThreadPool(int nThreads):	//固定初始化几个线程(固定大小线程池)
public static ExecutorService newSingleThreadExecutor(): //初始化一个线程的线程池(单线程线程池)

ExecutorService的提交与关闭:

<T> Future<T> submit(Callable<T> task); //提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。 
 Future<?> submit(Runnable task);//提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。 
void shutdown();//启动一次顺序关闭,执行以前提交的任务,但不接受新任务。 

Callable有返回值,Future中有一个get()方法可以获取返回的结果。

四、定时器

定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。开发中使用Quartz开源调度框架。
Timer:

public Timer(); //创建一个新的定时器
public void schedule(TimerTask task, long delay): //在指定毫秒值后执行定时任务
public void schedule(TimerTask task,long delay,long period); //在指定毫秒值delay后第一次执行任务,period毫秒后重复执行任务
public void schedule(TimerTask task,  Date time): //在指定日期后执行定时任务
public void schedule(TimerTask task,  Date firstTime, long period): //在指定日期后执行定时任务,相隔period毫秒后重复执行该任务
public void cancel(); //终止此定时器,丢弃当前已安排任务

TimeTask:

public abstract void run(); //此计时器要执行的任务
public boolean cancel(); //取消此定时器任务

五、内存可见性的问题

(一) Java内存模型

Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,
线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。
线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。
不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

(二) Java中的可见性

Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的, 当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。 另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

(三) volatile关键字

Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:
volatile不具有互斥性,但不能保证变量的原子性操作。JDK1.5之后可通过 java.util.concurrent.atomic包下的类来保证原子性操作。
java.util.concurrent.atomic 包下提供了一些原子操作的常用类:

  • AtomicBoolean 、 AtomicInteger 、 AtomicLong 、 AtomicReference
  • AtomicIntegerArray 、 AtomicLongArray
  • AtomicMarkableReference
  • AtomicReferenceArray
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值