JavaSE知识点总结
基本概念:程序、进程、线程
-
程序(program)
- 程序是为了完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码 进程(process)
-
进程是程序的一次执行或是正在运行的一个程序
进程的生命周期包括自身的产生、存在和消亡,如:运行中的QQ
程序是静态的,进程是动态的
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
线程(thread)
-
进程可进一步细化为线程,是一个程序内部的一条执行路径
若一个进程同一时间并行执行多个线程,就是支持多线程的
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小
一个进程中的多个线程共享相同的内存单元,它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患
单核CPU和多核CPU
-
单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。
例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果有某个人不想交钱,那么收费 人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为CPU时间单元特别短,因此感觉不出来
如果是多核的话,能够更好的发挥多线程的效率
一个Java应用程序java.exe,其实至少有三个线程 main()主线程, gc()垃圾回收线程, 异常处理线程。当然如果发生异常,会影响主线程
- 获取JVM中当前运行的所有线程
class Test {
public static Thread[] findAllThreads() {
ThreadGroup group = Thread.currentThread().getThreadGroup();
ThreadGroup topGroup = group;
// 遍历线程组树,获取根线程组
while (group != null) {
topGroup = group;
group = group.getParent();
}
// 激活的线程数加倍
int estimatedSize = topGroup.activeCount() * 2;
Thread[] slackList = new Thread[estimatedSize];
//获取根线程组的所有线程
int actualSize = topGroup.enumerate(slackList);
// copy into a list that is the exact size
Thread[] list = new Thread[actualSize];
System.arraycopy(slackList, 0, list, 0, actualSize);
//线程名 优先级 组 用途
//Reference Handler,10,system JVM在创建main线程后就创建Reference Handler线程,主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题
//Finalizer,8,system Finalizer线程在main线程之后创建的,主要用于在垃圾收集前,调用对象的finalize()方法
//Signal Dispatcher,9,system Attach Listener线程接收外部JVM命令,当命令接收成功后,会交给Signal Dispatcher线程去进行分发到各个不同的模块处理命令,并且返回处理结果
//Attach Listener,5,system 负责接收外部命令
//main,5,main
//Monitor Ctrl-Break,5,main Monitor Ctrl-Break线程是在idea中才有的
return list;
}
}
-
并行与并发
-
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事
并发:一个CPU同时(一段时间)执行多个任务
使用多线程的优点
-
1.提高应用程序的响应。对图形化界面更有意义,可增强用户体验
2.提高计算机系统CPU的利用率
3.改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
4.提高程序的处理效率
何时需要多线程
-
程序需要同时执行两个或多个任务,例外:
单核CPU使用单个线程先后完成多个任务,肯定比用多个线程来完成用的时间更短
程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等
需要一些后台运行的程序时
线程的创建和使用
- JVM允许程序运行多个线程,通过java.lang.Thread类来实现
-
Thread类的特性
-
每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式
- Thread类构造器
方法 | 解释 |
---|---|
Thread() | 创建新的Thread对象 |
Thread(String threadname) | 创建线程并指定线程实例名 |
Thread(Runnable target) | 指定创建线程的目标对象,目标对象实现Runnable接口中的run方法 |
Thread(Runnable target, String name) | 创建新的Thread对象 |
-
JDK5之前创建新执行线程有两种方法
-
继承Thread类的方式
实现Runnable接口的方式
创建线程方式一:继承Thread类
-
步骤
-
定义子类继承Thread类
子类中重写Thread类中的run()方法
创建Thread子类对象,即创建了线程对象
调用线程对象start()方法,start()方法创建完栈空间就结束,栈自动调用run()方法
// 1.定义子类继承Thread类
class ThreadOther extends Thread {
@Override
// 2.子类中重写Thread类中的run方法
public void run() {
for (int i = 0; i < 1000000; i++) {
System.out.println("分支线程" + i);
}
}
}
class MyThread {
//主线程
public static void main(String[] args) {
// 3.创建Thread子类对象,即创建了线程对象
ThreadOther threadOther = new ThreadOther();
// 4.调用线程对象start方法:启动线程,自动调用run()方法
threadOther.start();
for (int i = 0; i < 1000000; i++) {
System.out.println("主线程" + i);
}
}
}
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式
- run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定
- 想要启动多线程,必须调用start()方法
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出IllegalThreadStateException异常
创建线程方式二:实现Runnable接口
-
步骤
-
定义子类,实现Runnable接口
子类中重写Runnable接口中的run()方法
通过Thread类含参构造器创建线程对象
将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中
调用Thread类的start()方法,开启线程调用Runnable子类接口的run()方法,start()方法创建完栈空间就结束,栈自动调用run()方法
// 1.定义子类,实现Runnable接口
class ThreadOther implements Runnable {
@Override
// 2.子类中重写Runnable接口中的run()方法
public void run() {
for (int i = 0; i < 1000000; i++) {
System.out.println("分支线程" + i);
}
}
}
class MyThread {
//主线程
public static void main(String[] args) {
// 3.通过Thread类含参构造器创建线程对象
// 4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中
Thread thread = new Thread(new ThreadOther());
// 5.调用Thread类的start()方法:开启线程调用Runnable子类接口的run()方法
thread.start();
for (int i = 0; i < 1000000; i++) {
System.out.println("主线程" + i);
}
}
}
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式
- run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定
- 想要启动多线程,必须调用start()方法
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出IllegalThreadStateException异常
-
继承方式和实现方式
-
区别:
继承Thread:线程代码存放Thread子类run()方法中
实现Runnable:线程代码存在接口的子类的run()方法中
实现方式的好处:
避免了单继承的局限性
多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源
两种方式的缺点:
执行完任务之后无法获取执行结果
Thread类有关的方法
方法 | 解释 |
---|---|
void start() | 启动线程,并执行对象的run()方法 |
run() | 线程在被调度时执行的操作 |
String getName () | 返回线程的名称 |
void setName(String) | 设置线程名称 |
String getName() | 获取线程的名字 |
static Thread currentThread () | 返回当前正在执行线程的引用 |
static void yield() | 线程让步,暂停当前正在执行的线程,从运行态切换到就绪态,把执行机会让给优先级相同或更高的线程,若队列中没有同优先级的线程,忽略此方法,不释放锁 |
join() | 某个线程调用其他线程对象的join()方法时,该线程将被阻塞,直到调用其他线程执行完为止,释放锁,低优先级的线程也可以获得执行 |
static void sleep(long millis) | 当前活动线程在指定时间段内放弃对CPU控制,不释放锁,抛出InterruptedException异常 |
void interrupt() | 终止线程的睡眠,让Thread.sleep(Long)报中断异常 |
stop() | 强制线程生命期结束,不推荐使用,缺点是会丢失数据。 |
boolean isAlive() | 返回boolean ,判断线程是否还活着 |
-
线程调度
-
调度策略:抢占式(抢占CPU)、时间片
Java的调度方法:同优先级线程组成先进先出队列,使用时间片策略,对高优先级,使用优先调度的抢占式策略
线程优先级涉及的方法
-
getPriority():返回线程优先值
setPriority(int newPriority):改变线程的优先级
线程创建时继承父线程的优先级
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
Java中线程的优先级等级
-
MAX_PRIORITY: 10
MIN_PRIORITY: 1
NORM_PRIORITY: 5
线程分类
-
Java中的线程分为两类:一种是守护线程,一种是用户线程
它们在 几乎每个方面都是相同的,区别为 守护线程是依赖于用户线程,所有用户线程退出了,守护线程也就会退出, 用户线程是独立存在的,不会因为其他用户线程退出而退出
守护线程和用户线程一样,任务结束后也会死亡
守护线程是用来服务用户线程的,通过在调用start()方法前调用thread.setDaemon(true),可以把一个用户线程变成一个守护线程
Java垃圾回收线程就是一个典型的守护线程
若JVM中都是守护线程,当前JVM将退出
线程的声明周期
-
JDK中Thread.State枚举类定义了线程的六种状态
- 新生new、运行runnable、阻塞blocked、一直等待waiting、超时等待time_waiting、终止terminated 线程的一个完整的生命周期大体要经历如下的五种状态
-
新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时便进入运行状态,run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
/**
* 另一种线程死亡的方式
*/
class SleepOver {
public static void main(String[] args) {
MRunnable mRunnable = new MRunnable();
Thread thread = new Thread(mRunnable);
thread.start();
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
mRunnable.is = false;
}
}
class MRunnable implements Runnable {
boolean is = true;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (is) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
} else {
return;
}
}
}
}
线程的同步
- 多线程环境下可能出现的安全问题:当一个线程操作多条共享数据的语句,该线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误
- 解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行,即同步机制
- 局部变量永远不存在线程安全问题,成员变量可能存在线程安全问题。因为形成线程安全问题要满足多线程对共享的数据有修改的行为
- Java中,线程之间栈内存不共享(局部变量线程安全),堆内存和方法区共享(成员变量线程不安全)
-
Synchronized的使用方法
-
在静态方法声明中加synchronized,加的是类锁,类锁只有一把,在成员方法声明中加synchronized,加的是对象锁
synchronized会导致程序执行效率降低,尽量局部变量代替成员变量,多个对象代替一个对象
class B {
public void show1() {
//同步代码块
synchronized (对象或类,不能是null) {
//需要被同步的代码;
}
}
//synchronized还可以放在方法声明中,表整个方法为同步方法
public synchronized void show2() {
//需要被同步的代码;
}
}
-
同步机制中的锁
-
同步锁机制:
并发中防止共享资源竞争冲突的方法就是当资源被一个任务使用时在其上加锁。第一个访问某项资源的任务必须锁定这项资源使其他任务在其被解锁之前,无法访问这项资源,而在其被解锁之时,另一个任务就可以锁定并使用它了
synchronized的锁是什么:
任意对象都可以作为同步锁。所有对象都自动含有单一的锁监视器
同步方法的锁:静态方法是类锁、非静态方法是对象锁
同步代码块:自己指定
注意:
必须确保使用同一个资源的多个线程共用一把锁,否则无法保证共享资源的安全
同步的范围
-
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中其他线程不可以参与执行,即所有操作共享数据的这些语句都要放在同步范围中
切记
范围太小:没锁住所有有安全问题的代码
范围太大:没发挥多线程的功能
释放锁的操作
-
当前线程的同步方法、同步代码块执行结束
当前线程在同步代码块、同步方法中遇到break、 return终止了该代码块、该方法的执行
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception导致异常结束
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁
不会释放锁的操作
-
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁,应尽量避免使用suspend()挂起和resume()继续执行来控制线程
/**
* 单例模式
*/
class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
-
线程的死锁问题
-
死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决方法
专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步
synchronized最好不要嵌套使用,容易发生死锁
class DeadLockTest {
public static void main(String[] args) {
final StringBuffer s1 = new StringBuffer();
final StringBuffer s2 = new StringBuffer();
new Thread() {
public void run() {
synchronized (s1) {
s2.append("A");
synchronized (s2) {
s2.append("B");
System.out.print(s1);
System.out.print(s2);
}
}
}
}.start();
new Thread() {
public void run() {
synchronized (s2) {
s2.append("C");
synchronized (s1) {
s1.append("D");
System.out.print(s2);
System.out.print(s1);
}
}
}
}.start();
}
}
-
Lock(锁)
-
从JDK5开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步,同步锁使用Lock对象充当
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
class A {
private final ReentrantLock lock = new ReentrantLock();
public void m() {
lock.lock();
try {
//保证线程安全的代码;
} finally {
//将unlock()写入finally语句块,防止同步代码有异常
lock.unlock();
}
}
}
-
synchronized与Lock的对比
-
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
Lock只有代码块锁,synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序
线程的通信
-
wait()、notify()、notifyAll()
-
wait():让当前线程挂起、放弃CPU、放弃同步资源,使别的线程可访问并修改共享资源。之后当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行
notify():唤醒正在排队等待同步资源的线程中某一个线程,不能确切的唤醒某一个等待状态的线程,是 由JVM确定唤醒哪个线程,而且不是按优先级
notifyAll():唤醒正在排队等待资源的所有线程结束等待
这三个方法只有 在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常
这三个方法 只能在非静态的方法中使用
这三个方法只能在Object类中声明,每个对象中都有wait()和notify()方法
wait()方法
-
使当前活动的线程进入等待状态,直到另一线程对该对象发出notify()或notifyAll()为止
调用wait()方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
调用此方法后,当前线程将释放对象监控权 ,然后进入等待
在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行
notify()或notifyAll()
-
功能:唤醒等待该对象监控权的一个或所有线程
调用notify()或notifyAll()方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
notifyAll()不会唤醒等待其他对象监控权的线程
JDK5新增线程创建方式
创建线程方式三:实现Callable接口
-
与使用Runnable相比, Callable功能更强大些
-
相比run()方法,可以有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
但效率低
Future接口
-
可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等
FutrueTask是Futrue接口的唯一的实现类
FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
//1.创建一个实现Callable的实现类
class MyCallable implements Callable<Integer> {
//2.实现call方法,将线程需要执行的操作声明在call方法中
@Override
public Integer call() throws Exception {
return 1;
}
}
class Test1 {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
MyCallable myCallable = new MyCallable();
//4.将此Callable实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(myCallable);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start方法
Thread thread = new Thread(futureTask);
thread.start();
try {
//6.获取Callable中的call方法的返回值
//get方法的返回值为FutureTask构造器参数Callable实现类重写的call方法的返回值。
Object o = futureTask.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
创建线程方式四:使用线程池
-
背景:并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建和销毁线程会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间
-
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用
-
好处:
提高响应速度减少了创建新线程的时间
降低资源消耗,重复利用线程池中线程,不需要每次都创建
便于线程管理,corePoolSize核心池的大小,maximumPoolSize最大线程数,keepAliveTime线程没有任务时最多保持多长时间后会终止 -
JDK5起提供了线程池相关API
Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,该方法用来执行传进去的任务的
ExecutorService接口继承了Executor接口,并声明了一些方法:submit()、invokeAll()、invokeAny()以及shutDown()等
抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法
ThreadPoolExecutor继承了类AbstractExecutorService,重写了部分方法 -
ThreadPoolExecutor线程池类相关API
方法 | 解释 |
---|---|
void execute(Runnable command) | 执行任务/命令,没有返回值,用来执行Runnable |
Future submit(Callable task) | 执行任务,有返回值,用来执行Callable |
void shutdown() | 关闭连接池 |
- Executors工具类、线程池的工厂类,用于创建并返回不同类型的线程池
方法 | 解释 |
---|---|
Executors.newCachedThreadPool() | 创建一个可根据需要创建新线程的线程池 |
Executors.newFixedThreadPool(n) | 创建一个可重用固定线程数的线程池 |
Executors.newSingleThreadExecutor() | 创建一个只有一个线程的线程池 |
Executors.newScheduledThreadPool(n) | 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行 |
class Test4 {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5));
for (int i = 0; i < 15; i++) {
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println("线程池中线程数目:" + executor.getPoolSize() + ",队列中等待执行的任务数目:" +
executor.getQueue().size() + ",已执行玩别的任务数目:" + executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
System.out.println("正在执行task " + taskNum);
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task " + taskNum + "执行完毕");
}
}
面试题
1.模拟银行取钱的问题
/**
* 定义一个Account类
* 该Account类封装了账户编号(String)和余额(double)两个属性
* 设置相应属性的getter和setter方法
* 提供无参和有两个参数的构造器
* 系统根据账号判断与用户是否匹配,需提供hashCode()和equals()方法的重写
* 提供两个取钱的线程类:小明、小明的妻子
* 提供了Account类的account属性和double类的取款额的属性
* 提供带线程名的构造器
* run()方法中提供取钱的操作
* 在主类中创建线程进行测试。考虑线程安全问题
*/
class Account {
private String accountId;
private double balance;
public Account() {
}
public Account(String accountId, double balance) {
this.accountId = accountId;
this.balance = balance;
}
public String getAccountId() {
return accountId;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public String toString() {
return "Account [accountId=" + accountId + ", balance=" + balance + "]";
}
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((accountId == null) ? 0 : accountId.hashCode());
long temp;
temp = Double.doubleToLongBits(balance);
result = prime * result + (int) (temp ^ (temp >>> 32));
return result;
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Account other = (Account) obj;
if (accountId == null) {
if (other.accountId != null)
return false;
} else if (!accountId.equals(other.accountId)) return false;
if (Double.doubleToLongBits(balance) !=
Double.doubleToLongBits(other.balance))
return false;
return true;
}
}
class WithDrawThread extends Thread {
Account account;
// 要取款的额度
double withDraw;
public WithDrawThread(String name, Account account, double amt) {
super(name);
this.account = account;
this.withDraw = amt;
}
public void run() {
synchronized (account) {
if (account.getBalance() > withDraw) {
System.out.println(Thread.currentThread().getName() + ":取款成功,取现的金额为:" + withDraw);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.setBalance(account.getBalance() - withDraw);
} else {
System.out.println("取现额度超过账户余额,取款失败");
}
System.out.println("现在账户的余额为:" + account.getBalance());
}
}
}
class WithDrawThreadTest {
public static void main(String[] args) {
Account account = new Account("1234567", 10000);
Thread t1 = new WithDrawThread("小明", account, 8000);
Thread t2 = new WithDrawThread("小明's wife", account, 2800);
t1.start();
t2.start();
}
}
2.使用两个线程交替打印1-100
class Communication implements Runnable {
int i = 1;
public void run() {
while (true) {
synchronized (this) {
notify();
if (i <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + i++);
} else {
break;
}
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
3.经典例题:生产者/消费者问题
/**
* 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品
* 店员一次只能持有固定数量的产品(比如:20)
* 如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产
* 如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品
*/
// 售货员
class Clerk {
private int product = 0;
public synchronized void addProduct() {
if (product >= 20) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
product++;
System.out.println("生产者生产了第" + product + "个产品");
notifyAll();
}
}
public synchronized void getProduct() {
if (this.product <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("消费者取走了第" + product + "个产品");
product--;
notifyAll();
}
}
}
// 生产者
class Productor implements Runnable {
Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("生产者开始生产产品");
while (true) {
try {
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.addProduct();
}
}
}
// 消费者
class Consumer implements Runnable {
Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("消费者开始取走产品");
while (true) {
try {
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.getProduct();
}
}
}
class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Thread productorThread = new Thread(new Productor(clerk));
Thread consumerThread = new Thread(new Consumer(clerk));
productorThread.start();
consumerThread.start();
}
}
4.stop()和suspend()方法为何不推荐使用
- stop()不安全,它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程在那种状态下检查和修改它们,结果很难检查出真正的问题所在
- 假如一个线程正在执行synchronized void{x = 3; y = 4;},由于方法是同步的,多个线程访问时总能保证x,y被同时赋值,而如果一个线程正在执行到x = 3;时,被调用了stop()方法,即使在同步块中,它也干脆地stop()了,这样就产生了不完整的残废数据
- suspend()方法容易发生死锁,调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程
5.sleep()和wait()有什么区别
- sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放锁
- wait是Object类的方法,对象调用wait()方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify()方法或notifyAll()后本线程才进入对象锁定池准备获得对象锁进入运行状态
6.同步和异步有何异同,在什么情况下分别使用他们?举例说明
- 如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取
- 当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率
7.当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法
- 一个线程在访问一个对象的同步方法时,另一个线程可以同时访问这个对象的非同步方法
- 一个线程在访问一个对象的同步方法时,另一个线程不能同时访问这个同步方法
- 多个线程所持有的对象锁共享且唯一,一个线程在访问一个对象的同步方法时,另一个线程不能同时访问这个对象的其他同步方法
8.请说出你所知道的线程同步的方法
- wait():使一个线程处于等待状态,并且释放所持有的对象的锁
- sleep():使一个正在运行的线程处于阻塞状态,是一个静态方法,调用此方法要捕获InterruptedException异常,不释放所持有的对象的锁
- notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级
- notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争
9.简述synchronized和java.util.concurrent.locks.Lock的异同
- 主要相同点:Lock能完成synchronized所实现的所有功能
- 主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放
10.判断题
- C和Java都是多线程语言
错误,C是单线程语言 - 如果线程死亡,它便不能运行
正确,线程死亡就意味着它不能运行 - 在Java中,高优先级的可运行线程会抢占低优先级线程
正确 - 程序开发者必须创建一个线程去管理内存的分配
错误,Java提供了一个系统线程来管理内存的分配 - 一个线程可以调用yield()方法使其他线程有机会运行
正确
11.Java为什么要引入线程机制
- 线程可以彼此独立的执行,它是一种实现并发机制的有效手段,可以同时使用多个线程来完成不同的任务
12.Runnable接口包括哪些抽象方法?Thread类有哪些方法
- Runnable接口中仅有run()抽象方法
- Thread类主要方法有start()、run()、sleep()、currentThread()、setPriority()、getPriority()、join()、yield()、setName()、isAlive()等
13.在多线程中,为什么要引入同步机制
- 在多线程环境中,可能会出现两个甚至更多的线程试图同时访问同一个资源,必须对这种潜在的资源冲突进行预防
- 同步机制可以实现线程之间的通信,同步机制保证共享资源只能被一个线程所持有,避免了多线程执行时数据的不安全问题
14.wait()、notify()、notifyAll()的作用分别是什么
- wait():在获得锁的情况下,调用wait()方法的线程会将自身挂起,释放锁。直到获取相同锁的别的线程调用notify()或notifyAll(),该线程才有可能被唤醒
- notify():在获取锁的情况下,当前线程调用notify()会随机唤醒一个在等待相同锁的线程,但是不能保证被唤醒的线程会有执行权限,被唤醒的线程还是需要和别的线程竞争资源才有可能获得锁
- notifyAll():在获取锁的情况下,当前线程调用会唤醒等待同一把锁而进入沉睡的所有线程
- 补充:sychronized代码块中使用wait()、notify()等方法应该是加锁对象的方法,不一定是线程类对象的wait()等方法