线程的详细介绍
前言
线程的现象:
多线程,说白了就是多条执行路径,原来是一条路径(就如单线程),就主路径(main),现在是多条路径。就相当于高速路。原来是一条路,因为车多了,为提高使用效率,充分使
用这条道路,中间加了个栅栏, 变成了多条车道。
提示:以下是本篇文章正文内容,下面案例可供参考
一、常用概念
2.1. 程序
Java源程序和字节码文件被称为“程序” ( Program ),是
一个静态的概念为了能够达到效果,我们需要电脑去执行读取程序,并执行相应的操作。
2.2. 进程
执行中的程序叫做进程(Process),是一个动态的概念。
为了使计算机程序得以运行,计算机需要加载代码,同
时也要加载数据。
进程是程序的一次动态执行过程, 占用特定的地址空
间。
每个进程由3部分组成:cpu,data,code。每个进程都
是独立的,保有自己的cpu时间,代码和数据,即便
用同一份程序产生好几个进程,它们之间还是拥有自
己的这3样东西。
多任务(Multitasking)操作系统将CPU时间动态地划分
给每个进程,操作系统同时执行多个进程,每个进程
独立运行。以进程的观点来看,它会以为自己独占
Cpu的使用权。
进程的查看:
Windows系统: Ctrl+Alt+Del
Unix系统: ps or top
2.3. 线程
线程(英语:thread)是操作系统能够进行运算调度的
最小单位。它被包含在进程之中,是进程中的实际运作
单位。一条线程指的是进程中一个单一顺序的控制流,
一个进程中可以并发多个线程,每条线程并行执行不同
的任务。
Threads run at the same time, independently of one
another
一个进程可拥有多个并行的(concurrent)线程
一个进程中的线程共享相同的内存单元/内存地址空
间可以访问相同的变量和对象,而且它们从同一堆中
分配对象通信、数据交换、同步操作
由于线程间的通信是在同一地址空间上进行的,所以
不需要额外的通信机制,这就使得通信更简便而且信
息传递的速度也更快。
2.4. 多线程优缺点
2.4.1. 优点
资源利用率更好;程序设计在某些情况下更简单;程序响
应更快
2.4.2. 缺点
设计更复杂,虽然有一些多线程应用程序比单线程的
应用程序要简单,但其他的一般都更复杂。在多线程
访问共享数据的时候,这部分代码需要特别的注意。
线程之间的交互往 往非常复杂。不正确的线程同步产
生的错误非常难以被发现,并且重现以修复。
上下文切换的开销 当 CPU 从执行一个线程切换到执
行另外一个线程的时候,它需要 先存储当前线程的本
地的数据,程序 指针等,然后载入另一个线程的本地
数据,程序指针 等,最后才开始执行。这种切换称
为“上下文切 换”(“context switch”)。CPU 会在一 个上
下文中执行一个线程,然后切换到另外一个上下文中
执 行另外一个线程。上下文切换 并不廉价。如果没
有必要,应该减少上下文切换的发生。
二、创建线程
编写多线程程序是为了实现多任务的并发执行,从而能够更好地与用户交互。一般有四种方法,Thread , Runnable , Callable ,使用 Executor 框架来创建线程池。
2.1. 继承Thread类实现
- 创建线程类: 继承 Thread类 +重写 run() 方法
- 构造线程类对象: 创建 子类的对象
- 启动线程: 通过子类对象调用 start() 方法
创建 Thread 子类的一个实例并重写 run 方法, run
方法会在调用 start() 方法之后自动被执行
代码如下(示例):
public class TestThread {
public static void main(String[] args) {
// 创建线程类对象
SomeThread oneThread = new
SomeThread();
// 启动线程
oneThread.start();
}
}
// 创建线程类
class SomeThead extends Thread{
@Override
public void run()
{
//do something here
}
}
至此,一个线程就创建完成了。
这种方式的特点:那就是如果我们的类已经从一个类继
承(如小程序必须继承自 Applet 类),则无法再继承
Thread 类,异常只能捕获。
3.2. 实现Runnable接口实现
- 创建实现 Runnable 接口的实现类 + 重写 run() 方
法
public static void main(String[] args) {
// 创建线程类对象
SomeThread oneThread = new
SomeThread();
// 启动线程
oneThread.start();
}
}
// 创建线程类
class SomeThead extends Thread{
@Override
public void run()
{
//do something here
}
}
- 创建一个实现类对象
- 利用实现类对象创建Thread类对象
- 启动线程
public class TestThread2 implements Runnable {
SomeRunnable r1 = new SomeRunnable();
Thread thread1 = new Thread(r1);
thread1.start();
Thread thread2 = new Thread(new
SomeRunnable());
thread2.start();
}
// 创建Runnable子类
class SomeRunnable implements Runnable{
@Override
public void run()
{
//do something here
}
}
至此,一个线程就创建完成了。
线程的执行流程很简单,当执行代码oneThread.start();
时,就会执行oneRunnable对象中的void run();方法,该
方法执行完成后,线程就消亡了。
3.3. 实现Callable接口实现(了解)
- 创建实现 Callable 接口的实现类 + 重写 call() 方
法 - 创建一个实现类对象
- 由 Callable 创建一个 FutureTask 对象
- 由 FutureTask 创建一个 Thread 对象
- 启动线程
public class CallAbleTest {
public static void main(String[] args)
throws Exception{
MyCallable callable = new
MyCallable();
// 将Callable包装成FutureTask,
FutureTask也是一种Runnable
FutureTask<Integer> futureTask = new
FutureTask<>(callable);
// 将FutureTask包装成Thread
new Thread(futureTask).start();
System.out.println(futureTask.isDone());
System.out.println(futureTask.get());
}
}
class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100000; i++) {
sum += i;
}
return sum;
}
}
4.线程的五种状态
我们在现实生活中,思考问题、发现问题、处理问题,
这是一个完成一件事或者处理一个问题经历的中间过
程。在程序世界也一样,要完成一件事情线程从出生到
消亡会经历一个流程,中间会有不同状态的转换。
- 新建状态
使用 new 关键字和 Thread 类或其子类建立一个线程
对象后,该线程对象就处于新建状态。它保持这个状
态直到程序 start() 这个线程。 - 就绪状态
当线程对象调用了start()方法之后,该线程就进入就
绪状态。就绪状态的线程处于就绪队列中,要等待
JVM里线程调度器的调度。 - 运行状态
如果就绪状态的线程获取 CPU 资源,就可以执行
run(),此时线程便处于运行状态。处于运行状态的线
程最为复杂,它可以变为阻塞状态、就绪状态和死亡
状态。 - 阻塞状态
如果一个线程执行了sleep(睡眠)、suspend(挂
起)等方法,失去所占用资源之后,该线程就从运行
状态进入阻塞状态。在睡眠时间已到或获得设备资源
后可以重新进入就绪状态。可以分为三种:
等待阻塞:运行状态中的线程执行 wait() 方法,
使线程进入到等待阻塞状态。
同步阻塞:线程在获取 synchronized同步锁失败
(因为同步锁被其他线程占用)。
其他阻塞:通过调用线程的 sleep() 或 join() 发出
了 I/O请求时,线程就会进入到阻塞状态。当
sleep() 状态超时,join() 等待线程终止或超时,或
者 I/O 处理完毕,线程重新转入就绪状态。 - 死亡状态
一个运行状态的线程完成任务或者其他终止条件发生
时,该线程就切换到终止状态。
5.1停止线程
死亡状态是线程生命周期中的最后一个阶段。线程死亡
的原因有两个。一个是正常运行的线程完成了它的全部
工作;另一个是线程被强制终止,如通过执行 stop 或
destroy 方法来终止一个线程。但是,不要调用 stop,
destory 方法 ,太暴力,一盆冷水让其停止。
5.2. 阻塞状态
处于运行状态的线程在某些情况下,如执行了sleep(睡
眠)方法,或等待I/O设备等资源,将让出CPU并暂时停
止自己的运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞
的原因消除时,如睡眠时间已到,或等待的I/O设备空闲
下来,线程便转入就绪状态,重新到就绪队列中排队等
待,被系统选中后从原来停止的位置开始继续运行。
有三种方法可以让我们暂停Thread执行:
sleep方法:sleep() 方法需要指定等待的时间,它可
以让当前正在执行的线程在指定的时间内暂停执行,
进入阻塞状态,该方法既可以让其他同优先级或者高
优先级的线程得到执行的机会,也可以让低优先级的
线程得到执行机会。但是 sleep() 方法不会释放“锁标
t1.start(); //就绪状态
for(int i=0;i<1000;i++){
System.out.println(i);
}
ttc.terminate();
System.out.println(“ttc stop!”);
}
}
志”,也就是说如果有 synchronized 同步块,其他线
程仍然不能访问共享数据。
yield方法: yield() 方法和 sleep() 方法类似,也不会释
放“锁标志”,区别在于,它没有参数,即 yield() 方法
只是使当前线程重新回到可执行状态,所以执行
yield() 的线程有可能在进入到可执行状态后马上又被
执行。让出CPU的使用权,从运行态直接进入就绪
态。让CPU重新挑选哪一个线程进入运行状态。
join方法: 方法会使当前线程等待调用 join() 方法的线
程执行结束之后,才会继续往后执行。
6. 线程同步和死锁问题
6.1. 线程安全
在一般情况下,创建一个线程是不能提高程序的执行效
率的,所以要创建多个线程。但是多个线程同时运行的
时候可能调用线程函数,在多个线程同时对同一个内存
地址进行写入,由于CPU时间调度上的问题,写入数据
会被多次的覆盖,所以就有可能造成数据的不准确。
/**
* 测试同步问题
* @author Administrator
*
*/
public class TestSync {
public static void main(String[] args) {
Account a1 = new Account(100,"高");
Drawing draw1 = new Drawing(80,a1);
Drawing draw2 = new Drawing(80,a1);
draw1.start(); //你取钱
draw2.start(); //你老婆取钱
}
}
/*
* 简单表示银行账户, 将来打算多个线程共用的资源
*/
class Account {
int money;
String aname;
public Account(int money, String aname) {
super();
this.money = money;
this.aname = aname;
}
}
/**
* 模拟提款操作
* @author Administrator
*
*/
class Drawing extends Thread{
int drawingNum; //取多少钱
Account account; //要取钱的账户
int expenseTotal; //总共取的钱数
public Drawing(int drawingNum,Account
account) {
super();
this.drawingNum = drawingNum;
this.account = account;
}
@Override
public void run() {
5.2. 线程同步 synchronized
if(account.money-drawingNum<0){
return;
}
try {
Thread.sleep(1000); //判断完后阻
塞。其他线程开始运行。
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money-=drawingNum;
expenseTotal+=drawingNum;
System.out.println(this.getName()+"--
账户余额:"+account.money);
System.out.println(this.getName()+"--
总共取了:"+expenseTotal);
}
}
结果:
Thread-0–账户余额:20
Thread-1–账户余额:-60
Thread-0–总共取了:80
Thread-1–总共取了:80
7.1线程同步 synchronized
if(account.money-drawingNum<0){
return;
}
try {
Thread.sleep(1000); //判断完后阻
塞。其他线程开始运行。
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money-=drawingNum;
expenseTotal+=drawingNum;
System.out.println(this.getName()+"–
账户余额:"+account.money);
System.out.println(this.getName()+"–
总共取了:"+expenseTotal);
}
}
结果:
Thread-0–账户余额:20
Thread-1–账户余额:-60
Thread-0–总共取了:80
Thread-1–总共取了:80
线程同步:即当有一个线程在对内存进行操作时,其他
线程都不可以对这个内存地址进行操作,直到该线程完
成操作, 其他线程才能对该内存地址进行操作,而其他
线程又处于等待状态。
同步就是协同步调,按预定的先后次序进行运行。如:
你说完,我再说。
“同”字从字面上容易理解为一起动作
其实不是,“同”字应是指协同、协助、互相配合。
在Java里面,通过 synchronized 进行同步的保证。它
包括两种用法:synchronized 方法和 synchronized 块
7.2 synchronized 方法
通过在方法声明中加入 synchronized关键字来声明
synchronized 方法。如:
public synchronized void accessVal(int
newVal);
synchronized 方法控制对类成员变量的访问:每个对象
对应一把锁,每个 synchronized 方法都必须获得调用该
方法的对象的锁方能执行,否则所属线程阻塞,方法一
旦执行,就独占该锁,直到从该方法返回时才将锁释
放,此后被阻塞的线程方能获得 该锁,重新进入可执行
状态。
synchronized 方法的缺陷:若将一个大的方法声明为
synchronized 将会大大影响效率,典型地,若将线程类
的方法 run() 声明为 synchronized ,由于在线程的整个
生命期内它一直在运行,因此将导致它对本类任何
synchronized 方法的调用都永远不会成功。当然我们可
以通过将访问类成员变量的代码放到专门的方法中,将
其声明为 synchronized ,并在主方法中调用来解决这一
问题,但是 Java 为我们提供了更好的解决办法,那就是
synchronized 块。
7.3synchronized 块
在代码块前加上 synchronized 关键字,并指定加锁的
对象
synchronized(syncObject){
//允许访问控制的代码
}
/**
* 测试同步问题
* @author Administrator
*
*/
public class TestSync {
public static void main(String[] args) {
Account a1 = new Account(100,"高");
Drawing draw1 = new Drawing(80,a1);
Drawing draw2 = new Drawing(80,a1);
draw1.start(); //你取钱
draw2.start(); //你老婆取钱
}
}
/*
* 简单表示银行账户
*/
class Account {
int money;
String aname;
public Account(int money, String aname) {
super();
this.money = money;
this.aname = aname;
}
}
/**
* 模拟提款操作
* @author Administrator
*
*/
class Drawing extends Thread{
int drawingNum; //取多少钱
Account account; //要取钱的账户
int expenseTotal; //总共取的钱数
public Drawing(int drawingNum,Account
account) {
super();
this.drawingNum = drawingNum;
this.account = account;
}
@Override
public void run() {
draw();
}
// 改进使用 双重检查
void draw(){
synchronized (account) {
if(account.money-drawingNum<0){
return;
}
try {
Thread.sleep(1000); //判断完后
阻塞。其他线程开始运行。
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money-=drawingNum;
expenseTotal+=drawingNum;
}
System.out.println(this.getName()+"--
账户余额:"+account.money);
System.out.println(this.getName()+"--
总共取了:"+expenseTotal);
}
}