关于Java的线程和锁

7 篇文章 1 订阅
1 篇文章 0 订阅

1、线程、进程

进程是资源分配的基本单位,一个进程可以包含多个线程,每条线程执行不同的任务,不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。
线程是进程中执行运算的最小单位,虽然同一进程中的线程共享该进程的所有资源,但是每个线程也有自己独立的栈内存,用来存放本地数据。

2、创建线程的三种方式(实现多线程的四种方式)

(1)继承Thread类,重写run()方法
(2)实现Runnable接口,重写run()方法,【 实现Runnable接口的实现类的实例对象作为Thread的构造器的参数来创建线程,如:Thread thread = new Thread( new RunnableDemo() ) ; 】
(3)实现Callable接口,重写call方法,并使用FutureTask创建线程和获取返回值,而且callable还可以抛出异常。

	FutureTask<Integer> ft = new FutureTask<>(new MyTask());		//使用FutureTask运行线程并获得返回值
	Thread t = new Thread(ft);
	t.start();
	//自己获取返回值
	System.out.println(ft.get());

(4)通过线程池来创建线程

3、线程的生命周期

线程的生命周期

1、线程的一生会经历5个状态

  1. 新建:使用new关键字创建了一个线程。
  2. 就绪:调用start()方法之后,线程处于就绪状态,不一定立即运行,要等待资源调度运行
  3. 运行:就绪状态的线程获得CPU资源后,执行run方法
  4. 阻塞:当前线程失去所占的资源,暂停运行,进入阻塞状态
  5. 死亡:线程执行完毕或被其他线程杀死,线程死亡。

2、状态之间的转换以及一些方法

(1)Thread类中的方法:

  • sleep();:当线程调用sleep(time),线程从运行状态进入阻塞(休眠)状态,释放所占资源,使其他线程有执行机会;但是如果有锁的话,sleep方法并不会释放锁。
  • yield();:使当前线程从运行状态转换为就绪状态,只能使同优先级线程或更高优先级线程有执行机会,所以线程yield之后可能又马上被执行;yield方法也不会释放锁资源。
  • join();:先执行完调用该方法的线程,再执行其他线程。

(2)Object类中的方法:

  • wait()、notify()和notifyAll(): 必须在synchronizeed代码块或synchronized方法中使用。

首先了解两个概念:锁池和等待池
(1)锁池:假设线程A已经拥有了某个对象的锁,而其他线程想要获得该对象的锁,但是该线程的锁现在正被A所拥有,所以这些线程就进入了该对象的锁池中。等线程A释放了该对象的锁,在锁池中的线程才能去竞争该对象的锁。
(2)等待池:假设线程A调用了某个对象的wait方法,则线程A就会释放该对象的锁,然后进入该对象的等待池中,并且不能竞争锁。

  • wait(): wait()方法使当前线程进入阻塞状态并且释放锁,当前线程进入该对象的等待池,等待池中的线程不会去竞争锁资源。
  • notify(): 线程调用对象的notify方法时,会随机唤醒一个wait的线程(即随机唤醒一个在等待池中的线程),被唤醒的线程便会进入该对象的锁池中,可以参与该对象的锁资源的竞争。
  • notifyAll(): 与notify不同的时,该方法会唤醒对象的等待池中的所有对象,即等待池中所有的对象都将进入锁池中,参与锁资源的竞争。
    注意:优先级别高的线程竞争到锁资源的概率大,没有竞争到锁资源的线程还会在锁池中。

(3)wait()与sleep()的比较

  1. wait方法是Object类中的方法,由final修饰,可以被所有类继承,但不能被重写;
    而sleep是Thread类中的方法,由native修饰的本地方法。
  2. wait方法导致线程暂停,使用notify方法唤醒,会释放锁资源;
    而sleep方法导致线程睡眠一段时间,自动醒来,不会释放锁资源。
  3. 使用wait时,当前线程一定要先获得该对象的锁,即wait方法的调用必须在synchronized方法或代码块中使用。

4、线程死锁问题:

什么是死锁: 指两个或两个以上的进程在执行时,因为争抢资源而造成的互相等待的现象,若无外力作用,它们都将无法推进下去。

死锁产生的条件:

  1. 互斥条件:一个资源每次只能被一个进程所使用
  2. 请求与保持条件:一个进程因请求资源而被阻塞时,对已获得的资源保持不放
  3. 不可抢占条件:进程已获得资源,没有被使用完之前,不能强行抢夺
  4. 循环等待条件:若干线程形成一种头尾相接的循环等待资源关系

避免死锁最好的办法就是破坏循环等待条件,将系统中所有的资源设置标志位,排序,规定所有的进程申请资源时必须按照一定的顺序。

5、synchronized和ReentrantLock

首先,了解锁的类型:

1、可重入锁:在执行对象中所有同步方法时不用再次获得锁
2、可中断锁:在等待获取锁的过程中可中断
3、公平锁:按等到获取锁的线程的等待时间进行获取,等待时间长的优先获取锁
4、读写锁:读的时候可以多线程一起读,写的时候只能同步的写。

还有一种说法:悲观锁与乐观锁

1、悲观锁:每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。
2、乐观锁:每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。

写操作频繁时用乐观锁,读频繁频繁时用悲观锁

类别synchronizedLock
存在层次Java的关键字,在jvm层面上是一个类
锁的释放1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁在finally中必须释放锁,不然容易造成线程死锁
锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待
锁状态无法判断可以判断
锁类型可重入 不可中断 非公平 悲观锁可重入 可判断 可公平(两者皆可)乐观锁
性能少量同步大量同步

6、关于线程池

首先了解一下为什么需要用到线程池。

创建线程需要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程有限。为了避免这些问题,在程序启动的时候,我们就创建若干个线程来响应处理,它们被称为线程池,里面的线程称为工作线程。

假设一个服务器完成任务需要时间为:T1创建线程,T2线程中执行任务,T3销毁线程。

若,T1+T3远大于T2 ,则可采用线程池,以提高服务器性能。
线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或一些空闲的时间段。

线程池中最核心的类:java.util.concurrent.ThreadPoolExecutor

线程池中几个重要的参数:

  1. corePoolSize:核心线程数
    默认情况下,创建了线程池之后,线程池中的线程数为0,当有任务来的时候,就创建线程去执行它,当线程池中的线程数目达到核心线程数,则将到达的任务放入阻塞队列中。如果调用了prestartAllCoreThreads()或者prestartCoreThread()方法,会预创建所有核心线程数的线程或一个核心线程

  2. maximumPoolSize:最大线程数
    线程池中最大线程数,表示线程池中最多能创建多少线程;当线程池中核心线程+新建线程数=最大线程数时,这时再传进来任务,线程池就会拒绝新任务。

  3. keepAliveTime:非核心线程的空闲线程的存活时间
    默认情况下,当线程池中的线程数大于核心线程时,才会起作用;表示空闲线程能够存活的时间,直到线程数不超过核心线程数。如果调用allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

  4. unit:时间单位
    是keepAliveTime 的时间单位

  5. workQueue:阻塞队列,用来存储等待执行的任务的
    一个阻塞队列,用来存储等待执行的任务的,一般使用LinkedBlockingQueue

public class ThreadPool{
	public static void main(String[] args) {
		/*线程池的几个重要的参数:
			1、corePoolSize:核心线程数
			2、maximumPoolSize:最大线程数
			3、keepAliveTime:非核心线程的空闲线程存活时间
			4、unit:时间单位
			5、workQueue:阻塞队列,用来存储等待执行的任务
		*
		*/
		ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 2, 3, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3));
		//执行第一个任务,使用已存在的核心线程来执行
		pool.execute(new MyThread());
		//又执行2、3、4个任务,这几个任务进入时,要被放在阻塞队列中,等待执行(最终还是被核心线程执行)
		pool.execute(new MyThread());
		pool.execute(new MyThread());
		pool.execute(new MyThread());
		//执行第5个任务,此时创建新线程来执行任务(注意:这时创建的新线程2和核心线程1会分摊执行任务,不一定核心线程1 会执行前4个任务)
		pool.execute(new MyThread());
		//上面传入5个任务,导致核心线程数+新创建线程数=2(最大线程数),所以此时要是再传入任务,线程池会拒绝新任务
//		pool.execute(new MyThread());
		pool.shutdown();
	}
}

@SuppressWarnings("serial")
class MyThread implements Runnable,Serializable{
	@Override
	public void run() {
		System.out.println("当前执行此任务的线程:"+Thread.currentThread().getName());
	}
}

7、CAS

CAS是什么?
CAS是英文单词CompareAndSwap的缩写;是一种实现并发算法时常用到的技术,CAS有三个操作数:
当前内存值V,期望值A,新值B
CAS指令执行时,当且仅当要更新的值V与期望值A相等时,才能将内存值V修改为新值B,否则什么都不做。整个CAS操作就是一个原子操作。

CAS的缺点:

1、循环时间长,开销大:

 若CAS失败,会一直尝试,长时间不成功,会浪费资源和时间。

2、只能保证一个共享变量的原子操作

 当有多个共享原子变量需要保证原子性时,需要用锁。

3、ABA问题(重):

 若读到的内存值V为A,最后准备赋值时读取还是A,不一定就是没有被其他线程更改过,可能这段时间内,它的值被改为B,又被改回A,CAS操作就会误以为它从来没有改变过,这就是ABA问题。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值