线程
线程的定义
线程的产生
-
上世纪60年代,在计算机上运行的基本单位都是进程,它拥有独立的内存空间,但是随着计算机的发展,进程满足不了开发的需要:
- 进程因为拥有独立的资源,在创建进程、终止进程和切换进程时会产生较大的时间开销和空间开销。
- 计算机诞生初期,任何安装任何操作系统和软件,只能运行机器指令,完成一些简单的数学运算。受到当时价格因素的制约,计算机并不普及,拥有者主要是政府、大型机构和公司,一台计算机往往由多个用户共同使用。由于对称多处理机(SMP)出现,可以满足多个运行单位的运行,而多个进程并行开销过大。
所以引入了相对于进程较为轻型即开销较小的实体单位。在80年代,出现了能独立运行的基本单位——线程(Threads)。
线程的特点
-
轻型实体
-
线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。相对于进程来说更加的轻量。
-
线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。TCB包括以下信息:
(1)线程状态。
(2)当线程不运行时,被保存的现场资源。
(3)一组执行堆栈。
(4)存放每个线程的局部变量主存区。
(5)访问同一个进程中的主存和其它资源。
-
-
独立调度和分派的基本单位
- 在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。
-
可并发执行
- 进程与线程是一对多的关系,一个进程可以拥有多个线程,而这些线程都可以用来独立运行。大大提高了cpu的利用率。
-
共享进程资源
- 一个进程中的多个线程可以共享进程的资源,进程中的代码、数据、进程空间、文件都可以仅从访问,而利用这个特性,一个进程中的线程之间可以进行相互通信。
- 代码:即应用程序的代码;
- 数据:包括全局变量、函数内的静态变量、堆空间的数据等;
- 进程空间:操作系统分配给进程的内存空间;
- 打开的文件:各个线程打开的文件资源,也可以为所有线程所共享,例如线程 A 打开的文件允许线程 B 进行读写操作。
- 一个进程中的多个线程可以共享进程的资源,进程中的代码、数据、进程空间、文件都可以仅从访问,而利用这个特性,一个进程中的线程之间可以进行相互通信。
在线程引入之后,进程与线程之间形成了1对n的包含关系。当进程中仅包含 1 个执行程序指令的线程时,该线程又称“主线程”,这样的进程称为“单线程进程”。进程仅负责为各个线程提供所需的资源,真正执行任务的是线程,而不是进程。
多线程和并发
-
多线程,表示进程中拥有(>=2)的线程数,线程之间相互协作,共同执行程序。
-
并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
-
高并发是一种系统执行的状态,有以下的缺点:
1.危险:多个线程共享数据可能会产生与预期结果不相符的结果
2.活跃性:某个操作无法进行下去,就有可能发生死锁,饥饿等问题
3.性能:线程过多时会使得,CPU频繁切换,调度时间增多,同步机制,消耗过多内存
Thread类
Thread类处于java.lang.Thread下
public class Thread extends Object implements Runnable
线程 是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。
每个线程都有一个优先级,高优先级线程的执行优先于低优先级线程。每个线程都可以或不可以标记为一个守护程序。当某个线程中运行的代码创建一个新 Thread
对象时,该新线程的初始优先级被设定为创建线程的优先级,并且当且仅当创建线程是守护线程时,新线程才是守护程序。
当 Java 虚拟机启动时,通常都会有单个非守护线程(它通常会调用某个指定类的 main
方法)。Java 虚拟机会继续执行线程,直到下列任一情况出现时为止:
- 调用了
Runtime
类的exit
方法,并且安全管理器允许退出操作发生。 - 非守护线程的所有线程都已停止运行,无论是通过从对 run 方法的调用中返回,还是通过抛出一个传播到
run
方法之外的异常。
创建新执行线程有两种方法。一种方法是将类声明为 Thread
的子类。该子类应重写 Thread
类的 run
方法。
Runnable接口
public interface **Runnable**
Runnable
接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run
的无参数方法。
设计该接口的目的是为希望在活动时执行代码的对象提供一个公共协议。例如,Thread
类实现了 Runnable
。激活的意思是说某个线程已启动并且尚未停止。
此外,Runnable
为非 Thread
子类的类提供了一种激活方式。通过实例化某个 Thread
实例并将自身作为运行目标,就可以运行实现 Runnable
的类而无需创建 Thread
的子类。大多数情况下,如果只想重写 run()
方法,而不重写其他 Thread
方法,那么应使用 Runnable
接口。这很重要,因为除非程序员打算修改或增强类的基本行为,否则不应为该类创建子类。
我们可以使用匿名内部类
去实现Runnable接口里的run方法。
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Person s = ps[index];
synchronized (cond){
try {
Thread.sleep(10);
cond.buyTicket(s);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
thread.start();
线程锁
在开发中,高并发的环境是避免不了的,我们处理并发的手段除了优化算法和减少系统开销外,还能通过同步异步来处理线程的执行顺序,利用系统的闲置资源,来达到按顺序执行,避免不必要的系统开销。
-
同步是阻塞模式,
同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返 回信息,那么Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。,直到收到返回信息才继续执行下去;
-
异步是非阻塞模式。
异步是指进程不需要一直等下去, 而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。
java虚拟机的线程调度
- Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。
这种抢占性调度具有很强的随机性,当一个线程执行完后,下一个时间片的归属完全由线程之间抢占决定,如果不规定优先级,那么就会造成多个线程抢占cpu,往往程序不会按照我们决定的顺序执行。当线程过多时,可能会造成饥饿现象。
而除了抢占cpu以外,进程中的数据,该进程的线程也能够使用,数据相对于线程来说是公共的,也就是说会出现当一个线程没有执行完的时候,另一个线程很可能就会已经计算将数据修改,就会产生线程安全问题
。
- 线程安全:线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
面对线程安全问题,计算机给出了锁的这个概念。
锁
饥饿现象与死锁
- 饥饿是指系统不能保证某个进程的等待时间上界,从而使该进程长时间等待,当等待时间给进程推进和响应带来明显影响时,称发生了进程饥饿。当饥饿到一定程度的进程所赋予的任务即使完成也不再具有实际意义时称该进程被饿死。
- 死锁是指在多道程序系统中,一组进程中的每一个进程都无限期等待被该组进程中的另一个进程所占有且永远不会释放的资源。
-
相同点:二者都是因为竞争资源引起的。
-
不同点:首先死锁是同步的,饥饿时异步的。
- 从进程状态考虑,死锁进程都处于等待状态,饥饿的进程处于运行或就绪状态,只有部分能够执行。
- 死锁进程等待永远不会被释放的资源,饿死进程等待会被释放但却不会释放分配给自己的资源。
- 死锁一定发生了循环等待,而饿死则不然。
- 死锁一定涉及多个进程,而饥饿或被饿死的进程可能只有一个。(在同一个进程中竞争)
- 在饥饿的情形下,系统中有至少一个进程能正常运行,只是饥饿进程得不到执行机会。而死锁则可能会最终使整个系统陷入死锁并崩溃
关于饥饿和死锁的文章参考:多线程饥饿现象,饥饿与死锁区别
synchronized锁
synchronized是Java中很早就出现的锁,它的主要特性有三:
- 1)、原子性:**所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。**被
synchronized
修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放。 - (2)、可见性:**可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。 **synchronized和volatile都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性。
- (3)、有序性:有序性值程序执行的顺序按照代码先后执行。 synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
synchronized使用方法
-
修饰实例方法:对实例对象进行加锁,进入方法内部执行之前,需要获得实例对象。
public synchronized void buyTicket(Person p) throws InterruptedException
-
修饰静态方法:对当前类加锁,由于static是用来修饰表示方法、变量是静态的,在加载类时,static首先会被执行,表明static是属于类的静态资源,被static修饰的方法、变量只属于类,不属于任何实例对象,所以使用synchronized修饰的静态方法是用来锁定类的。
public synchronized static void buyTicket(Person p) throws InterruptedException
-
修饰代码块:这种情况要根据代码块前的括号里参数来定,如果里面是一个实例对象,那么加锁的对象就是该实例对象,如果里面是一个(类名.class),那么锁的就是该类。
Conductor cond = new Conductor(tempmap); ..... synchronized (cond){ try { Thread.sleep(10); cond.buyTicket(s); } catch (InterruptedException e) { throw new RuntimeException(e); } }
synchronized (Conductor.class){ try { Thread.sleep(10); cond.buyTicket(s); } catch (InterruptedException e) { throw new RuntimeException(e); } }
关于加锁实例对象和加锁的类的区别,这两者其实就是1对n的关系,类只有一个,而类的实例对象可以有n个。如果加锁的是A类的实例a,那么只有获得a对象才能访问,如果加锁的是这个类,那么即使拥有实例对象,也需要去等待。只有一个线程执行结束、其他的才能够调用同步的部分。
对象锁示例:
public void timethread() throws InterruptedException {
Long starttime = System.currentTimeMillis();//调用系统类获得开始的毫秒值
synchronized (this){//对象锁,对应main方法中的t8
Long endtime = System.currentTimeMillis();//调用系统类获得结束的毫秒值
System.out.println(Thread.currentThread().getName()+"运行和等待时间:"+(endtime-starttime));//打印线程名和运行和等待时间
Thread.sleep(500);//睡眠增加间隔对照
}
}
public static void main(String[] args) {
Text8 t8 = new Text8();//创建唯一一个实例对象
for (int i = 0; i < 10; i++) {//for循环创建线程
Thread thread = new Thread(new Runnable() {//使用内部类实现Runnable接口
@Override
public void run() {
try {
t8.timethread();//调用方法
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
}
}
我们通过synchronized块,来对实例对象t8加上对象锁,而我们for循环中创建的每个线程都要去使用t8调用timethread方法,多个线程请求获得t8,只能进行等待,等待线程运行完后获得对象抢占时间片。
运行结果:
每个线程运行时都使用Sleep睡眠了500ms,我们去除一些创建线程的不规律的时间开销,发现线程都是成等差数列的递增,这是由于在获得对象之前,就计算了starttime,表示开始等待的毫秒值,在获得对象后计算获得时间endtime毫秒值,相减得出总共等待的时间。结果显示通过对象锁线程在没有获得对象时都在等待,实现了同步。
类锁实例:
public void timethreadclass() throws InterruptedException {
Long starttime = System.currentTimeMillis();//调用系统类获得开始的毫秒值
synchronized (Text9.class) {//对象锁,对应main方法中的t8
Long endtime = System.currentTimeMillis();//调用系统类获得结束的毫秒值
System.out.println(Thread.currentThread().getName() + "运行和等待时间:" + (endtime - starttime));//打印线程名和运行和等待时间
Thread.sleep(500);//睡眠增加间隔对照
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {//循环创建变量
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
new Text9().timethreadclass();//创建匿名对象调用方法
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
}
}
与对象锁的示例类似,不同的是,我们在synchronized ()
中的参数修改为Text9.class
,表示这是类锁,同时,我们在main方法中,实现了Runnable接口中的run()方法,并创建匿名对象去调用timethreadclass方法,由于是临时的匿名,每个对象都不相同。
运行结果:
结果同样表示,线程由于类锁的原因在等待,实现了同步。
java中的锁并不只是只有synchronized 一种,可以参考文章:
Java中的锁分类 - byhieg - 博客园 (cnblogs.com)
集合线程安全
我们在线程锁中讲到线程安全问题,在我们常见的开发中,有一些结构化的数据同样也有线程安全问题。
-
线程安全:Vector、HashTable、Properties
-
线程不安全:ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap
为了保证集合是线程安全的,相应的效率也比较低;线程不安全的集合效率相对会高一些。
- Collections工具类提供了包装方法,能够将线程不安全的集合转换为线程安全的集合
关于线程安全的文档参考自:
(38条消息) java中哪些集合是线程安全的,哪些是线程不安全的_城序猿的博客-优快云博客_下面哪个集合是线程安全的
如果文章对大家有所帮助,多多点赞哦!