文章目录
1 线程概念
1.1 进程与线程
进程:是一个正在执行中的程序。每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
线程:就是进程中的一个独立的执行单元。线程在控制着进程的执行。一个进程中至少有一个线程。
单线程与多线程:
单线程程序:程序只有一条执行路径
多线程程序:程序有多条执行路径
e.g:
Java VM 启动的时候会有一个进程java.exe.
该进程中至少一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。
扩展:其实更细节说明jvm,jvm启动不止一个线程,主线程在运行的同时,还有负责垃圾回收机制的线程。
JVM启动是多线程的,最低有两个线程启动了,包括主线程和垃圾回收线程
为什么使用多线程:同时运行多份代码,提高程序运行效率
1.2 创建线程(一)
通过对api的查找,java已经提供了对线程这类事物的描述。就Thread类。
1、创建线程的第一种方式:继承Thread类。
步骤:
- 定义类继承Thread。
- 复写Thread类中的run方法。
目的:将自定义代码存储在run方法。让线程运行。 - 调用线程的start方法,
该方法两个作用:启动线程,调用run方法。
2、运行情况:发现运行结果每一次都不同。
- 因为多个线程都获取cpu的执行权。cpu执行到谁,谁就运行。
明确一点,在某一个时刻,只能有一个程序在运行。(多核除外) - cpu在做着快速的切换,以达到看上去是同时运行的效果。
我们可以形象把多线程的运行行为在互相抢夺cpu的执行权。
这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长,cpu说的算。
3、为什么要覆盖run方法
Thread类用于描述线程。
该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是run方法。
也就是说Thread类中的run方法,用于存储线程要运行的代码。
class Demo extends Thread
{
public void run()
{
for(int x=0; x<60; x++)
System.out.println("demo run----"+x);
}
}
class ThreadDemo
{
public static void main(String[] args)
{
//for(int x=0; x<4000; x++)
//System.out.println("Hello World!");
Demo d = new Demo();//创建好一个线程。
//d.start();//开启线程并执行该线程的run方法。
d.run();//仅仅是对象调用方法。而线程创建了,并没有运行。
for(int x=0; x<60; x++)
System.out.println("Hello World!--"+x);
}
}
1.3 线程的五种状态

说明:
- 创建线程
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。 - 就绪状态
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度 - 运行状态
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。 - 阻塞状态
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态,需要被动 notify()唤醒才可以到运行状态。
同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。sleep()时间结束后自动进入运行状态。
1.4 获取线程名称
当我们创建多线程时,想要区分哪个线程,可以获取线程的名称
原来线程都有自己默认的名称。Thread-编号 该编号从0开始。
static Thread currentThread():获取当前线程对象。
getName(): 获取线程名称。
设置线程名称:setName或者构造函数。
Test(String name)
{
super(name);
}
public void run()
{
for(int x=0; x<60; x++)
{
System.out.println((Thread.currentThread()==this)+"..."+this.getName()+" run..."+x);
}
}
1.5 创建线程(二)
创建线程的第二种方式:实现Runable接口
步骤:
- 定义类实现Runnable接口
- 覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中。
- 通过Thread类建立线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
- 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
两种创建线程的实现方式和继承方式有什么区别呢?
实现方式好处:避免了单继承的局限性,不然就只能继承thread类了
在定义线程时,建立使用实现方式。
两种方式区别:
继承Thread:线程代码存放Thread子类run方法中。
实现Runnable:线程代码存在接口的子类的run方法。
/*买票小程序*/
class Ticket implements Runnable//extends Thread
{
private int tick = 100;
public void run()
{
while(true)
{
if(tick>0)
{
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
}
class TicketDemo{
public static void main(String[] args)
{
Ticket t = new Ticket();
//共享一个ticket资源
Thread t1 = new Thread(t);//创建了一个线程;
Thread t2 = new Thread(t);//创建了一个线程;
t1.start();
t2.start();
}
}
两种创建线程方式的总结

2 多线程安全问题
在订票小程序中通过分析发现,打印出0,-1,-2等错票。
多线程的运行出现了安全问题。
2.1 问题的原因
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
总结:
线程安全问题:
- 是否是多线程环境
- 是否有共享数据
- 是否有多条语句操作共享数据
2.2 解决办法
原则:对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。
Java对于多线程的安全问题提供了专业的解决方式。
就是同步代码块
synchronized(对象)
{
需要被同步的代码
}
对象如同锁。持有锁的线程可以在同步中执行。
没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
同步类比:火车上的卫生间—经典(厕所有人加锁无人开锁)。
同步的前提:必须保证同步中只能有一个线程在运行。
- 必须要有两个或者两个以上的线程。
- 必须是多个线程使用同一个锁。
好处:解决了多线程的安全问题。
弊端:当线程很多时,每个线程都要去判断锁,较为消耗资源,
class Ticket implements Runnable
{
private int tick = 1000;
Object obj = new Object();
public void run()
{
while(true)
{
synchronized(obj)
{
if(tick>0)
{
//try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
}
}
class TicketDemo2{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.start();
t2.start();
t3.start();
}
}
2.3 同步的两种方式
另外同步有两种方式,同步代码块和同步函数
1、同步函数用的是哪一个锁呢?
函数需要被对象调用。那么函数都有一个所属对象引用。就是this。
所以同步函数使用的锁是this。
同步代码块的锁是程序员指定的对象
class Bank
{
private int sum;
//Object obj = new Object();
public synchronized void add(int n)
{
//synchronized(obj)
//{
sum = sum + n;
try{Thread.sleep(10);}catch(Exception e){}
System.out.println("sum="+sum);
//}
}
}
2、如果同步函数被静态static修饰后,使用的锁是什么呢?
通过验证,发现不是this。因为静态方法中也不可以定义this。静态进内存是,内存中没有本类对象,但是一定有该类对应的字节码文件对象。类名.class 该对象的类型是Class
3、静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class
synchronized(Ticket.class)
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
}
}
如何确定同步问题:该程序是否有安全问题,如果有,如何解决?
如何找问题:
- 明确哪些代码是多线程运行代码。
- 明确共享数据。(一般成员变量会涉及到共享数据的问题)
- 明确多线程运行代码中哪些语句是操作共享数据的。
设计模式中关于同步的面试例子:
懒汉式的特点在于实例的延迟加载,懒汉式会有缺点,在多线程访问时,会出现安全问题。通过加同步代码块来解决,但是会有效率问题,可以用双重判断的方式,加的锁是该类的字节码文件对象。
class Single
{
private static Single s = null;
private Single(){}
public static Single getInstance()
{
if(s==null)
{
synchronized(Single.class)
{
if(s==null)
//--->A;
s = new Single();
}
}
return s;
}
}
2.4 线程安全类回顾
StringBuffer、Vector、HashTable
线程安全,但是效率比较低的

2.5 JDK5以后提供Lock接口
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
Lock:
void lock(): 获取锁。
void unlock():释放锁。
ReentrantLock是Lock的实现类.
2.6 死锁情况
同步弊端:效率低,同步中嵌套同步会产生死锁问题
死锁:
两个或两个以上的线程在争夺资源的过程中,发生的一种相互等待的现象。
public void run() {
if (flag) {
synchronized (MyLock.objA) {
System.out.println("if objA");
synchronized (MyLock.objB) {//A线程拿了A锁想要B锁
System.out.println("if objB");
}
}
} else {
synchronized (MyLock.objB) {
System.out.println("else objB");
synchronized (MyLock.objA) {//B线程拿了B锁想要A锁,形成互相等待
System.out.println("else objA");
}
}
}
}
3 线程间通信
线程间通信问题:不同种类的线程针对同一个资源的操作
多个线程操作同一个资源,做不同的操作
3.1 生产者与消费者
一些例子:电影院买票,只出不进;但是商家卖东西是有卖出,有进货
Java提供的等待唤醒机制:

wait():
notify();
notifyAll();
方法与锁相关,必须通过锁对象调用
为什么这些方法不定义在Thread类中呢?
这些方法的调用必须通过锁对象调用,而我们刚才使用的锁对象是任意对象,可以被任意对象调用的方法定义Object类中。
所以,这些方法必须定义在Object类中。
3.2 匿名内部类创建多线程
匿名内部类的格式:
new 类名或者接口名() {
重写方法;
};
本质:是该类或者接口的子类对象。
e.g
// 继承Thread类来实现多线程
new Thread() {
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + ":"
+ x);
}
}
}.start();
// 实现Runnable接口来实现多线程
new Thread(new Runnable() {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + ":"
+ x);
}
}
}) {
}.start();
4 线程池
线程池的好处:线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用
如何实现线程的代码呢?
- 创建一个线程池对象,控制要创建几个线程对象。
public static ExecutorService newFixedThreadPool(int nThreads) - 这种线程池的线程可以执行:
可以执行Runnable对象或者Callable对象代表的线程
做一个类实现Runnable接口。 - 调用如下方法即可
Future<?> submit(Runnable task) <T> Future<T> submit(Callable<T> task) - 我就要结束,可以吗?
可以。
e.g:
// 创建一个线程池对象,控制要创建几个线程对象。
// public static ExecutorService newFixedThreadPool(int nThreads)
ExecutorService pool = Executors.newFixedThreadPool(2);
// 可以执行Runnable对象或者Callable对象代表的线程
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
//结束线程池
pool.shutdown();
5 任务定时器
定时器:可以让我们在指定的时间做某件事情,还可以重复的做某件事情。
依赖Timer和TimerTask这两个类:
Timer:定时
public Timer()
public void schedule(TimerTask task,long delay)
public void schedule(TimerTask task,long delay,long period)
public void cancel()
TimerTask:任务
6 回顾
- 什么是多线程
- 有几种方式实现多线程,用代码实现
- 如何获取和设置线程的名称
- 线程的常见方法
- 线程的生命周期图
- 线程安全产生的原因
- 如何解决线程安全的问题
1:多线程有几种实现方案,分别是哪几种?
两种。
继承Thread类
实现Runnable接口
扩展一种:实现Callable接口。这个得和线程池结合。
2:同步有几种方式,分别是什么?
两种。
同步代码块
同步方法
3:启动一个线程是run()还是start()?它们的区别?
start();
run():封装了被线程执行的代码,直接调用仅仅是普通方法的调用
start():启动线程,并由JVM自动调用run()方法
4:sleep()和wait()方法的区别
sleep():必须指时间;不释放锁。
wait():可以不指定时间,也可以指定时间;释放锁。
5:为什么wait(),notify(),notifyAll()等方法都定义在Object类中
因为这些方法的调用是依赖于锁对象的,而同步代码块的锁对象是任意锁。
而Object代码任意的对象,所以,定义在这里面。
6:线程的生命周期图
新建 – 就绪 – 运行 – 死亡
新建 – 就绪 – 运行 – 阻塞 – 就绪 – 运行 – 死亡
建议:画图解释。

被折叠的 条评论
为什么被折叠?



