第三部分Java SE-Java应用
第4单元 线程(Thread)部分学习笔记
Part1 动手做任务
一、创建线程的两种方式(面试题)
Java语言内在支持多线程机制。
为什么提供两种创建线程的方式?
- 答:当一个类已经有了某个父类的话,就不能再继承Thread类了,只能实现Runnable接口。
1.方式一:继承Thread类,观察程序的输出
实现步骤:
- (1).继承父类Thread
- (2).重写run方法(线程主体)
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
/**
* 线程做的事情在run方法里写,此方法来自父类Thread,子类重写父类方法
*/
public void run() {
for (int i = 0; i < 10; i++) {
//Thread.currentThread().getName()得到当前线程的名字
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread("张三");
t1.start();//启动线程的方法
//t1.run();run方法能被调用,但是就失去了线程的意义
MyThread t2 = new MyThread("李四");
t2.start();
}
}
特别注意:启动线程用start()方法
2.方式二:实现Runnable接口,观察程序输出
实现步骤:
- (1).实现Runnable接口
- (2).创建类对象
- (3).用Thread类包装
public class MyThread1 implements Runnable {
@Override //实现接口中的run方法
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
Thread th1 = new Thread(t1,"张三");//必须有这一步
th1.start();
Thread th2 = new Thread(new MyThread1(),"李四");
th2.start();
}
}
3.线程调度简单分析
线程调度策略,与OS有关,执行代码的是CPU;
- 在某一个时间点上,CPU能同时执行多个程序吗?
- 答:不能,因此导致了执行过程不是确定的。
二、多个线程共享数据时引发的问题-非同步(非线程安全,效率较高)
重点:多个线程共享同一数据时,有可能产生共享问题。以下示例模拟多个线程共享一个银行账户。
场景:一个存折、一张银行卡对应的是同一个账户,同时取款。需要编写4个类:账户类,该类对象被共享;First和Second类,代表存折和银行卡;Test类是测试类,分析结果:
1.账户类Account:
public class Account {
/** 账户余额 */
private int balance;
public Account(int balance) {
this.balance = balance;
}
/**
* 取款方法
* @param count 要取款的金额
*/
public void withDraw(int count) {
System.out.println(Thread.currentThread().getName() + "进入取款方法。");
if (balance >= count) {
System.out.println(Thread.currentThread().getName() + "可以取款。");
balance = balance - count;
System.out.println(Thread.currentThread().getName() + ":取了" + count + ";结余:" + balance);
} else {
System.out.println(Thread.currentThread().getName() + "取款时余额不足!!!");
}
}
}
2.First类:代表存折
public class First extends Thread {
Account account;//银行账户
public First(Account account) {
this.account = account;//给账户初始化
}
public void run() {
account.withDraw(1000);//调用取款方法
}
}
3.Second类:代表银行卡
public class Second extends Thread {
Account account;
public Second(Account account) {
this.account = account;
}
public void run() {
account.withDraw(500);
}
}
4.Test测试类
public class Test {
public static void main(String[] args) {
Account acc = new Account(1000);//建立账户对象,余额1000,该对象被两个线程共享
First first = new First(acc);//下面两个进程共享同一账户
Second second = new Second(acc);
first.start();//启动线程
second.start();
}
}
- 可能的输出结果(输出结果是不确定的,多执行几次,看输出结果):
//Thread-1进入取款方法。
//Thread-0进入取款方法。
//Thread-1可以取款。
//Thread-0可以取款。
//Thread-1:取了500;结余:500
//Thread-0:取了1000;结余:-500 //余额已经不够要取得了,居然还能取,多线程共享数据引发问题
- 出现问题的原因:多线程共享同一数据引发的同步问题。线程的调度是由操作系统完成的,在存折线程执行取款过程中,有可能被银行卡线程打断,导致存折线程取款过程不连续,引发了上边的问题。
- 解决方案:使用线程同步,让存折取款过程不被打断;在Account类取款方法加入同步(synchronized)关键字即可。
三、如何实现线程同步(效率降低,线程安全)
synchronized是Java关键字,称为同步关键字,在多线程共享数据时使用。
1.方式一:同步整个方法-在方法前加synchronized
线程同步就是保证多个线程共享数据,不出问题;即共享数据是安全的。
public class Account {
/** 账户余额 */
private int balance;
public Account(int balance) {
this.balance = balance;
}
/**
* 取款方法
* @param count 要取款的金额
*/
public synchronized void withDraw(int count) {
System.out.println(Thread.currentThread().getName() + "进入取款方法。");
if (balance >= count) {
System.out.println(Thread.currentThread().getName() + "可以取款。");
balance = balance - count;
System.out.println(Thread.currentThread().getName() + ":取了" + count + ";结余:" + balance);
} else {
System.out.println(Thread.currentThread().getName() + "取款时余额不足!!!");
}
}
}
- 其他类没有变化,执行Test类:加上之后,取款过程不会被打断,便免了问题的发生。
2.方式二:同步局部代码(代码块)
Account类中的变化:
public class Account {
/** 账户余额 */
private int balance;
public Account(int balance) {
this.balance = balance;
}
/**
* 取款方法
* @param count 要取款的金额
*/
public void withDraw(int count) {
System.out.println(Thread.currentThread().getName() + "进入取款方法。");
//同步代码块
synchronized (this) {
if (balance >= count) {
System.out.println(Thread.currentThread().getName() + "可以取款。");
balance = balance - count;
System.out.println(Thread.currentThread().getName() + ":取了" + count + ";结余:" + balance);
} else {
System.out.println(Thread.currentThread().getName() + "取款时余额不足!!!");
}
}
}
}
四、sleep方法
线程“睡觉”方法,在“睡觉”过程中,不释放对象锁。
public class StudentThread extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
try {//sleep()方法 ,线程睡觉2秒钟
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
StudentThread st = new StudentThread();
st.start();
}
}
五、如何实现生产者-消费者问题
- 涉及到:线程通信问题,多个线程之间要彼此通信。使用到的方法:wait()、notify()、notifyAll()和线程同步synchronized。
阐述的问题:生产者生产产品,放到一个盒子里,这个盒子只能放一件商品;生产者通知消费者来取,消费者取走产品后,通知生产者,可以继续生产了。如果消费者没有取走,生产者就等待;如果生产者没有生产出产品,消费者等待。
- 共有4个类:服务员、厨师(生产者)、吃货(消费者)、测试类,厨师和吃货共享服务员
1.服务员类Clerk
//服务员类,其对象被生产者和消费者共享
public class Clerk {
private int product = -1;// -1 表示目前服务员手里没有产品
/**
* 这个方法由厨师(生产者)调用,此方法往服务员里放产品
* @param p 产品
*/
public synchronized void setProduct(int p) {
if (this.product != -1) {//服务员手里有东西
try {//服务员手里有东西,厨师需要等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.product = p;
System.out.printf("厨师(生产者)炒出(%d)%n",this.product);
notify();//通知等待区的一个吃货(消费者)可以继续了,,但只会有一个进程获得执行机会
}
/**
* 这个方法由吃货(消费者)调用,该方法从服务员手里取产品
* @return 产品
*/
public synchronized int getProduct() {
if (this.product == -1) {//服务员手里没有东西
try {//吃货需要等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int p = this.product;
System.out.printf("吃货(消费者)吃掉 (%d)%n",this.product);
System.out.println("");
this.product = -1;//取走产品
notifyAll();//通知所有生产者线程,可以继续生产了,但只会有一个线程会获得执行机会
return p;
}
}
2.生产者类Produce
//生产者(厨师)线程类
public class Producer implements Runnable {
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("厨师(生产者)开始炒制菜品。。。");
for (int produce = 1; produce <= 10; produce++) {
try {//暂停随机时间
Thread.sleep((int)Math.random() * 3000);
} catch (InterruptedException e) {
e.printStackTrace();
}//将产品交给服务员
clerk.setProduct(produce);
}
}
}
3.消费者类Consumer
//消费者(吃货)线程类
public class Consumer implements Runnable {
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
System.out.println("吃货(消费者)开始大吃。。。");
for (int i = 1; i <= 10; i++) {
try {//等待随机时间
Thread.sleep((int)Math.random() * 3000);
} catch (InterruptedException e) {
e.printStackTrace();
}//从服务员处取走产品
clerk.getProduct();
}
}
}
4.测试类Test
//测试类
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
//生产者线程
Thread pt = new Thread(new Producer(clerk));
//消费者线程
Thread ct = new Thread(new Consumer(clerk));
pt.start();
ct.start();
}
}
输出结果:
//吃货(消费者)开始大吃。。。
//厨师(生产者)开始炒制菜品。。。
//厨师(生产者)炒出(1)
//吃货(消费者)吃掉 (1)
//厨师(生产者)炒出(2)
//吃货(消费者)吃掉 (2)
//厨师(生产者)炒出(3)
//吃货(消费者)吃掉 (3)
//厨师(生产者)炒出(4)
//吃货(消费者)吃掉 (4)
//厨师(生产者)炒出(5)
//吃货(消费者)吃掉 (5)
//厨师(生产者)炒出(6)
//吃货(消费者)吃掉 (6)
//厨师(生产者)炒出(7)
//吃货(消费者)吃掉 (7)
//厨师(生产者)炒出(8)
//吃货(消费者)吃掉 (8)
//厨师(生产者)炒出(9)
//吃货(消费者)吃掉 (9)
//厨师(生产者)炒出(10)
//吃货(消费者)吃掉 (10)
Part2 口述部分(面试题)
1.程序、进程和线程的区别是什么?
- 程序:代码,实现某种功能。
- 进程:执行当中的程序,会被载入内存,对应一个进程,进程独占系统资源,不同的进程之间,资源不能共享,进程由操作系系统管理。
- 线程:线程包含在进程中,一个进程可能包含多个线程,多个线程共享系统资源,线程由操作系系统管理。多个线程可以“同时”执行。迅雷下载、网际快车FlashGet。
2.线程实现方式是什么?
- 两种方式:代码见上边
- (1)继承Thread类
- (2)实现Runnable接口
3.线程的状态(生命周期)有哪些?
- 面试点:线程有5个状态:创建、就绪、阻塞、运行、消亡;start()调完是就绪态。
4.什么是锁机制?如何理解锁机制?死锁?同步目的以及优缺点?
-
(1)答:即线程同步的机制:
-
(2)理解:synchronized机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对共享资源的访问都是顺序的。Java实现多线程的同步操作很简单,只要在需要同步的方法或代码块中加入synchronized关键字,它能保证在同一时刻最多只有一个进程执行同一个对象的同步代码,可保证不会被干扰。如果同步了,哪个线程获得锁,哪个线程才能执行。
-
(3)锁只有1个;死锁:即锁死了。死锁解决不了,只能预防;例如:银行家算法。
-
(4)同步的目的:防止多线程共享数据出现问题。比如:取款问题。
-
(5)同步的优缺点:
- 优点:数据不会出问题
- 缺点:效率降低
5.sleep、wait、 yield等方法的分析
- sleep()方法: 被调用时不释放锁,可以在任何地方调用;在睡眠期间一直保持状态,直到睡眠时间到。
- yield()方法-让位方法:不释放锁,当一个线程调用该方法后,这个线程会停下,它会观察当前其它线程的执行优先级高低;如果别的线程优先级高于自己,他就会停下等待;否则他继续执行。
- wait()方法: 被调用时释放锁,只能在同步方法或语句块中调动。
- join()方法:一个进程加入到另一个进程中。
- start()方法:启动进程。
6.线程方法和对象方法的区分
(1)线程方法:
- 来自Thread类中的方法:start() 、run() 、sleep() 、yield()
(2)对象方法:
- 来自Object类中的方法:wait()、notify() 、notifyAll()
7.如何结束线程?
- (1)自然结束:run方法执行完毕。
- (2)在run方法中发生异常。
8.什么场合使用同步?同步和异步的区别?
- (1)答:数据被多线程共享时使用同步。
- (2)区别:
- 同步就是保证多个线程共享数据,不出问题;即共享数据是安全的。效率降低,线程安全。
- 异步非线程安全,效率较高。
9.多线程共享时注意什么?
- 答:避免彼此的 打断,导致数据的紊乱。
10.什么是并发?
- 答:多线程同时执行,指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
11.Java线程锁有哪些?各有什么特点?
synchronized(给共享资源上锁)
- 常用于维护数据一致性,可保证修饰的代码在执行过程中不会被其他线程干扰。使用synchronized修饰的代码具有原子性和可见性。
ReentrantLock
- 可重入锁,顾名思义,这个锁可以被线程多次重复进入进行获取操作。
Semaphore:信号量与互斥锁
- 通过acquire()与release()方法来获得和释放临界资源,避免死锁和读脏数据。
AtomicInteger:实现锁
- 同步就是保证多个线程共享数据,不出问题;即共享数据是安全的。效率降低,线程安全。
- 异步非线程安全,效率较高。
9.多线程共享时注意什么?
- 答:避免彼此的 打断,导致数据的紊乱。
10.什么是并发?
- 答:多线程同时执行,指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
11.Java线程锁有哪些?各有什么特点?
synchronized(给共享资源上锁)
- 常用于维护数据一致性,可保证修饰的代码在执行过程中不会被其他线程干扰。使用synchronized修饰的代码具有原子性和可见性。
ReentrantLock
- 可重入锁,顾名思义,这个锁可以被线程多次重复进入进行获取操作。
Semaphore:信号量与互斥锁
- 通过acquire()与release()方法来获得和释放临界资源,避免死锁和读脏数据。