Java多线程基础知识总结

线程的状态

NEW

线程被创建,但是还未调用start()方法就处于此状态。

RUNNABLE

Java线程将操作系统中READY和RUNNING合称为RUNNABLE。线程调用start()方法后处于READY(可运行)状态。处于可运行状态的线程获得了CPU时间片(timeslice)就处于RUNNING(运行中)状态。
在这里插入图片描述

BLOCKED

等待获取同步监视器(锁)的线程处于此状态。

WAITING

表示线程处于等待状态。例如,调用Object.wait()、Thread.join()、LockSupport.park()后线程就处于此状态。进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

TIMED_WAITING

超时等待状态。例如,调用Object.wait(long)、Thread.join(long)、LockSupport.parkNanos、LockSupport.parkUntil后线程就处于此状态。该状态不同于WAITING,它是可以在指定时间内自行返回的。

TERMINATED

终止状态,表示线程已经执行完毕。

在这里插入图片描述

创建线程的方式

  1. 继承Thread类,重写run方法;
  2. 实现Runnable接口,重写run方法;
  3. 通过Callable和Future创建,可以获得返回值。

这里主要讲下第三种方式。线程的执行体是Callable接口的call()方法,但是Callable接口不是Runnable接口的子接口,所以它不能作为Thread类的target。call()方法有返回值,用Future接口来代表该方法的返回值,Future接口有一个实现类FutureTask,并且FutureTask也实现了Runnable接口,所以FutureTask可以作为Thread类的target。示例代码如下:

		FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
			@Override
			public Integer call() throws Exception {
				int i = 0;
				for (; i < 50; i++) {
					System.out.println(Thread.currentThread().getName() + " i的值:" + i);
				}
				return i;
			}
		});
		for (int i = 0; i < 50; i++) {
			System.out.println(Thread.currentThread().getName() + " i的值:" + i);
			if (i == 25) {
				new Thread(task, "有返回值的线程").start();
				Thread.yield();
			}
		}
		try {
			System.out.println("子线程返回值:" + task.get());
		} catch (InterruptedException | ExecutionException ex) {
			ex.printStackTrace();
		}

还有一种方法,通过线程池来提交Callable:

		ExecutorService es = Executors.newSingleThreadExecutor();
		Future<Integer> future = es.submit(new Callable<Integer>() {
			@Override
			public Integer call() throws Exception {
				int i = 0;
				for (; i < 99; i++) {
					System.out.println(Thread.currentThread().getName() + "->" + i);
				}
				return i;
			}
		});
		Integer rsp;
		try {
			rsp = future.get();
			System.out.println("返回值:" + rsp);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
		es.shutdown();

线程安全问题

多个线程操作同一份资源时会出现线程安全问题

线程同步

synchronized关键字

  1. synchronized可以加在方法上,也可以加在代码块上,所以分为同步方法同步代码块
  2. 当synchronized加在非静态方法上时,所用的锁是this,也就是该对象本身;当synchronized加在静态方法上时,所用的锁是该类的class对象。因此,同一个对象的几个不同的非静态方法都用synchronized修饰,这几个方法不能同时运行,静态方法同理。

Lock接口和ReadWriteLock接口

Java中的Lock和ReadWriteLock原理浅析

死锁

同步锁的嵌套,互相等待对方释放锁,会导致死锁,示例代码如下:

package com.example.demo;

public class DeadLock {
	
	protected static Object obj1 = new Object();
	protected static Object obj2 = new Object();
	
	public static void main(String[] args) {
		Thread th1 = new Thread(new Task1(), "A");
		Thread th2 = new Thread(new Task2(), "甲");
		th1.start();
		th2.start();
	}

}

class Task1 implements Runnable {

	@Override
	public void run() {
		String threadName = Thread.currentThread().getName();
		for (int i = 0; i < 50; i++) {
			synchronized(DeadLock.obj1) {
				System.out.println(threadName + "获取到锁obj1");
				synchronized(DeadLock.obj2) {
					System.out.println(threadName + "获取到锁obj2");
				}
			}
		}
	}
}

class Task2 implements Runnable {

	@Override
	public void run() {
		String threadName = Thread.currentThread().getName();
		for (int i = 0; i < 50; i++) {
			synchronized(DeadLock.obj2) {
				System.out.println(threadName + "获取到锁obj2");
				synchronized(DeadLock.obj1) {
					System.out.println(threadName + "获取到锁obj1");
				}
			}
		}
	}
}

控制线程

开启:start

开启线程,让线程处在就绪的状态

休眠:sleep

让线程休眠一段时间,sleep不会释放锁

让步:yield

让处于运行状态的线程放弃CPU执行机会,并重新竞争CPU执行机会,也就是让线程回到就绪状态。完全可能的情况是:当某个线程调用了yield()方法放弃CPU执行机会之后,立马又竞争到了CPU执行机会

等待某个线程先执行:join

当前线程会等待调用join方法的线程执行完才继续执行

设置优先级:setPriority

优先级高的线程获得较多的执行机会,而优先级低的线程获得较少的执行机会。setPriority的参数是一个int类型,范围是1~10,值越大优先级越高

后台线程

有一种线程,它是在后台运行,它的任务是为其他的线程提供服务,这种线程被称为“后台线程”,又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。后台线程有一个特征:如果所有的前台线程都死亡,后台线程会自动死亡。通过setDaemon(true)方法可以将指定线程设置为后台线程。

线程通信

传统的线程通信

下面三个方法都是Object类的方法,必须由同步锁对象来调用。

wait

导致当前线程阻塞,直到其他线程调用该同步锁的notify()或notifyAll()来唤醒该线程,或者超时时间到了自动苏醒(带参数的wait方法)

notify

唤醒在此同步锁上等待的单个线程。如果有多个线程在此同步锁上等待,则会随机唤醒其中一个线程。注意,被唤醒的线程要去竞争获得同步锁才能被执行。

notifyAll

唤醒在此同步锁上等待的所有线程。同样,被唤醒的线程要去竞争获得同步锁才能被执行。

一个经典案例

假设现在有个杯子,服务员往杯子里倒水,顾客用这个杯子喝水,约定只有喝完了服务员才会往里面倒水。所以应该是倒一次水,喝完之后再倒一次,然后再喝完再倒,这样依次循环。。。

package com.example.demo;

public class WaitNotifyTest {
	
	public static void main(String[] args) {
		Cup cup = new OldCup();
		
		Waiter waiterA = new Waiter(cup, "服务员A");
		Waiter waiterB = new Waiter(cup, "服务员B");
		Waiter waiterC = new Waiter(cup, "服务员C");
		Customer me = new Customer(cup, "我");
		Customer myWife = new Customer(cup, "我老婆");
		
		waiterA.start();
		waiterB.start();
		waiterC.start();
		me.start();
		myWife.start();
	}
}

interface Cup {
	void input(String name);
	void output(String name);
}

class OldCup implements Cup {
	
	private boolean empty = true;

	@Override
	public synchronized void input(String name) {
		//水杯有水,服务员等待
		if (!empty) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//下面这个判断是针对多个服务员给同一个杯子倒水的情况
		if (empty) {
			System.out.println(name + "给客户倒了一杯水");
			this.notifyAll();
			this.empty = false;
		}
	}
	
	@Override
	public synchronized void output(String name) {
		//水杯空了,等待服务员倒水
		if (empty) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//下面这个判断是针对多个人喝同一杯水的情况
		if (!empty) {
			System.out.println(name + "喝了一杯水");
			this.notifyAll();
			this.empty = true;
		}
	}
}

class Waiter extends Thread {
	
	private Cup cup;
	
	public Waiter(Cup cup, String name) {
		super(name);
		this.cup = cup;
	}
	
	@Override
	public void run() {
		for (int i = 1; i <= 50; i++) {
			cup.input(this.getName());
		}
	}
	
}

class Customer extends Thread {
	
	private Cup cup;
	
	public Customer(Cup cup, String name) {
		super(name);
		this.cup = cup;
	}
	
	@Override
	public void run() {
		for (int i = 1; i <= 50; i++) {
			cup.output(this.getName());
		}
	}
}

上面代码的运行结果就是服务员倒一次水,客人喝一次水,倒一次,喝一次。。。

使用Condition控制线程通信

如果程序不使用synchronized关键字,而是使用Lock来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait、notify、notifyAll等方法了,这时我们可以使用Condition类,Condition的三个方法await、signal、signalAll分别对应传统的wait、notify、notifyAll等方法。我们可以基于Lock把上面的经典案例做一些修改,只需要增加一个NewCup类,实现Cup接口:

class NewCup implements Cup {
	
	private final Lock lock = new ReentrantLock();
	
	private final Condition con = lock.newCondition();
	
	private boolean empty = true;

	@Override
	public void input(String name) {
		lock.lock();
		try {
			//水杯有水,服务员等待
			if (!empty) {
				try {
					con.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			//下面这个判断是针对多个服务员给同一个杯子倒水的情况
			if (empty) {
				System.out.println(name + "给客户倒了一杯水");
				con.signalAll();
				this.empty = false;
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
	@Override
	public void output(String name) {
		lock.lock();
		try {
			//水杯空了,等待服务员倒水
			if (empty) {
				try {
					con.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			//下面这个判断是针对多个人喝同一杯水的情况
			if (!empty) {
				System.out.println(name + "喝了一杯水");
				con.signalAll();
				this.empty = true;
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
}

运行上面示例,也可以得到倒一次水喝一次水效果。

volatile关键字

用于修饰一个变量。保证了不同线程对变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。但是volatile不能保证原子性,因为就算线程的工作内存能够立即从主内存中load变量的新值,但是在往主内存写的时候还是会发生线程安全问题。volatile关键字现在用的少,主要是现在的计算机配置都比较好,主内存中的变量发生修改,会立马同步到各个线程的工作内存中。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值