在看了一些线程面试题后又来巩固基础,加深理解
1. 基本概念
- 程序(Program)
是为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。 - 进程(process)
是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在喝消亡过程(生命周期)。 - 线程(thread)
(1)进程可进一步细化为线程,是一个程序内部的一条执行路径。
(2)若一个进行同一时间并行执行多个线程,就是支持多线程的。
(3)线程作为调度和执行的单位,每个线程拥有独立的执行栈和程序计数器。线程切换的开销小。
一个进程中的多个线程共享相同的内存单元。 - 并行与并发
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事情。
并发:一个CPU(采用时间片)同时执行多个任务。
2. 创建多线程的三种方式
2.1 继承Thread类,重写run()方法
2.2 实现Runnable接口
方式一与方式二两种方式的对比:
开发中优先选择:实现Runnable接口的方式
原因:
(1)实现的方式没有类的单继承的局限性
(2) 实现的方式更适合来处理多个线程共享数据的情况
2.3 实现Callable接口(JDK1.5)
Callable接口实际是属于Executor框架中的功能类,Callable结构与Runnable接口的功能类似,但提供了比Runnable更强大的功能,主要体现在如下三点:
(1)Callable在任务结束后可以提供一个返回值,Runnable无法提供该功能。
(2)Callable中的call()方法可以跑出异常,而Runnable中的run()不能跑出异常。
( 3)运行Callable可以拿到一个Future对象,Future对象表示异步计算的结果,它提供能了检查计算是否完成的方法。由于线程输入异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下,就可以使用Future来监控目标线程来调用call()方法的情况,当调用Future的get()方法以获取结果时,当前线程会阻塞,直到目标线程的call()方法结束返回结果。
public class CallableAndFuture{
//创建线程类
public static class CallableTest implements Callable{
public String call() throws Exception{
return "Hello World!";
}
}
public static void main(String[] args){
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future<String> future = threadPool.submit(new CallableTest());
try{
System.out.println("waiting thread to finish");
System.out.println(future.get());
}catch{Exception e}{
e.printStackTrace
}
}
}
3. Thread类中常用的方法:
(1) start():启动当前线程,调用当前线程的run()方法。
(2) run():通常需要重新Thread类中的此方法,将创建的线程要执行的操作声明放在此方法中。
(3) currentThread():静态方法,返回执行当前代码的线程
(4) getName():获取当前线程的名字。
(5) setName():设置当前线程的名字
(6) yield():释放当前CPU的执行权。
(7) join():在线程a中调用线程b的jion(),此时线程a进入阻塞状态,知道线程b完全执行完一行a才结束阻塞状态。
(8) stop():已过时。当执行此方法时,强制结束当前线程。
(9) sleep(long millitime) :让当前线程“睡眠”指定的毫秒,在该时间内,当前线程时阻塞状态。
(10) isAlive():判断当前线程是否存活。
4. 线程的通信
详细参考连接
(1) wait():
(2)notify():
(3)notifyAll():
这三个方法定义在Object类中。
5. 线程的分类
守护线程和用户线程
6. 线程的生命周期
7. 线程的安全问题
(1)多个线程执行的不确定性引起执行结果的不稳定
案例:设置3个线程卖票,票数为:100张
《情景一》
public class Window1 implements Runnable{
private int tickets = 100;
@Override
public void run() {
while (true){
if(tickets > 0 ){
System.out.println(Thread.currentThread().getName()+"卖票,票号为:"+tickets);
tickets --;
}else {
break;
}
}
}
}
public class Window1Test {
public static void main(String[] args) {
Window1 task = new Window1();
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
Thread t3 = new Thread(task);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
此时:会发现有多张票号为100的票。
《情景二》
卖票的时候让线程休眠100毫秒:
public class Window1 implements Runnable{
private int tickets = 100;
@Override
public void run() {
while (true){
if(tickets > 0 ){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖票,票号为:"+tickets);
tickets --;
}else {
break;
}
}
}
}
此时:会有 票号为 :-1 的票出现
BUT:
8. 线程的安全问题如何解决?同步机制
当一个线程a在操作ticket的时候,其它线程不能参与进来,直到线程a操作完ticket,其它线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
在java中,我们通过同步机制来解决线程安全问题
方式一:同步代码块:synchronized( 同步监视器){需要被同步的代码}
- 什么叫需要被同步的代码:操作共享数据的代码。
- 什么叫共享数据:多个线程共同操作的变量
- 同步监视器:锁,任何一个类的对象,都可以称作锁(要求多个线程必须共用同一把锁)
public class Window1 implements Runnable{
private int tickets = 100;
@Override
public void run() {
while (true){
synchronized (this){
if(tickets > 0 ){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖票,票号为:"+tickets);
tickets --;
}else {
break;
}
}
}
}
}
方式二:同步方法:
此时我们要注意,同步代码块不能被synchronized“包裹”太多,这样会降低代码运行效率。
9. 线程安全的单例模式之懒汉模式
使用同步机制将单例模式中的懒汉式改写为线程安全的
(1)好处:懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。
(2)适用于:如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候使用懒汉模式就是一个不错的选择。
(3)缺点:但是这里的懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,因此需要加锁解决线程同步问题。
//创建多个线程,各自去调run()方法,在各自的run()方法中,又各自调了getInstance()方法
//假设有2个线程要调用getInstance()方法, 第一个线程首次调用是null,进入之后如果遇到线程阻塞情况,此时另一个线程进来了,遇到instance也是null,所以会导致 instance=new Bank();
//被先后执行2次初始化。
class Bank{
private Bank(){}
private static Bank instance = null;
public static synchronized Bank getInstance(){//类本身也充当一个对象,静态方法绑定在类上面
if(instance == null){
instance=new Bank();
}
return instance;
}
}
或
但是这个方式效率稍差。因为其实除了最前面的线程(不知道谁会抢到这个线程,第一个?第二个?),后面的大家都在排队等待,此时不需要进入同步代码块等待了,直接取走用就好了。也就是立一个牌子,告诉别人已经有实例了。
10. 线程的死锁问题
public class ThreadDeadLock {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
//线程1
new Thread(){
@Override
public void run() {
//锁1
synchronized (s1){
s1.append("a");
s2.append("1");
//演示阻塞,此时该线程在等着拿 s2,大家互相僵持
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//锁2
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println("s1====>"+s1);
System.out.println("s2====>"+s2);
}
}
}
}.start();
//线程2
new Thread((new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
//演示阻塞,此时该线程在等着拿 s1,大家互相僵持
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println("s1--->"+s1);
System.out.println("s2---"+s2);
}
}
}
})).start();
}
}
看起来比较“隐蔽”的代码案例:
import javax.swing.*;
class A {
public synchronized void foo(B b){//同步监视器:A类的对象 a
System.out.println("当前线程名:"+Thread.currentThread().getName()+"进入了A实例的foo方法");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程名:"+Thread.currentThread().getName()+"企图调用B实例的last方法");
b.last();
}
public synchronized void last(){//同步监视器:A类的对象 a
System.out.println("进入了A类的last方法內部");
}
}
class B{
public synchronized void bar(A a){//同步监视器:b
System.out.println("当前线程名:"+ Thread.currentThread().getName()+"进入了B实例的bar方法");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程名:"+Thread.currentThread().getName()+"企图调用A实例的last方法");
a.last();
}
public synchronized void last(){
System.out.println("进入了B类的last方法內部");
}
}
public class DeadLock implements Runnable{
A a = new A();
B b = new B();
public void init(){
Thread.currentThread().setName("主线程");
a.foo(b);
System.out.println("进入了主线程之后");
}
@Override
public void run() {
Thread.currentThread().setName("副线程");
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock d1 = new DeadLock();
new Thread((d1)).start();
d1.init();
}
}
主线程:要先获得A类对象a(同步监视器),再获得B类对象b(同步监视器)。
分线程:要先获得B类对象b(同步监视器),再获得A类对象a(同步监视器)。
他们两个正好相反。
11. 解决线程安全的另一种方式:Lock(锁)
JDK 5.0 新增
java.util.concurrent.locks.Lock 接口:
ReentrantLock类实现了Lock接口
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
public static void main(String[] args) {
Windows w= new Windows();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Windows implements Runnable{
private int tickets = 100;
// 1. 实例化 ReentrantLock
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
// 2.调用锁定方法 lock()
lock.lock();
if(tickets > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程售票,票号为:"+tickets);
tickets --;
}else {
break;
}
}finally {
//3. 调用解锁方法 unlock()
lock.unlock();
}
}
}
}
12. 线程练习题
版本一: 《自己瞎琢磨写的》
没有线程安全机制的时候:
import java.util.concurrent.locks.ReentrantLock;
public class AccountTest {
public static void main(String[] args) {
Account a = new Account();
Thread t1 = new Thread(a);
Thread t2 = new Thread(a);
t1.setName("用户1");
t2.setName("用户2");
t1.start();
t2.start();
}
}
class Account implements Runnable{
private double money = 0;
// private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
for(int i = 0; i<3;i++){
try {
// lock.lock();
Thread.sleep(100);
save();
System.out.println(Thread.currentThread().getName()+"向里面存钱==》"+"当前余额"+money);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// lock.unlock();
}
}
}
public void save(){
money += 1000;
}
}
打印结果:
加入线程安全机制后:
import java.util.concurrent.locks.ReentrantLock;
public class AccountTest {
public static void main(String[] args) {
Account a = new Account();
Thread t1 = new Thread(a);
Thread t2 = new Thread(a);
t1.setName("用户1");
t2.setName("用户2");
t1.start();
t2.start();
}
}
class Account implements Runnable{
private double money = 0;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
for(int i = 0; i<3;i++){
try {
lock.lock();
Thread.sleep(100);
save();
System.out.println(Thread.currentThread().getName()+"向里面存钱==》"+"当前余额"+money);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public void save(){
money += 1000;
}
}
版本二:正式答案版
public class AccountTest01 {
public static void main(String[] args) {
Account1 acct = new Account1(0);
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);
c1.setName("储户1");
c2.setName("储户2");
c1.start();
c2.start();
}
}
class Account1{
private double balance;
public Account1(double balance){
this.balance = balance;
}
public synchronized void deposit(double amt){
this.balance += amt;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"存钱成功,余额为:" + this.balance);
}
}
class Customer extends Thread{
private Account1 acct;
public Customer(Account1 acct){
this.acct = acct;
}
@Override
public void run() {
for(int i = 0;i<3;i++){
acct.deposit(1000);
}
}
}
13. 线程的通信
wait()、notify()、notifyAll()
定义在:java.lang.Object类中
只能出现在同步代码块或者同步方法中。
案例:线程1和线程2 交替打印1-100
wait()会释放锁,而sleep()不会.
当一个线程触发notify()的时候,尽管它将其他线程唤醒了,但是此时它已经拿到锁了,因此再有其他线程进入的时候也需要等待,直到执行 wait()的时候,锁才会被释放。
14. JDK1.5 新增线程的创建方式