线程
程序,进程,线程
概念:
程序:是完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。
进程:就是正在执行的程序,从Windows角度讲,进程是操作系统进行资源分配的最小单位。
线程:进程可进一步细化为线程,是一个进程内部的最小执行单位,是操作系统进行任务调度的最小单位,隶属于进程。
拓展:
进程的引入:在多道程序的环境时,程序的执行是并发执行,因此他们会失去封闭性,并具有间断性和运转结果不可再现性。通常,程序是不能参与并发执行的,否则,程序的执行就失去了意义。为了使程序可以并发执行,并且可以对并发执行的程序加以控制和描述,人们在OS中引入了“进程”这一概念。
线程的引入:20世纪60年代中期,人们设计多道程序设计OS时,引入了进程的概念,从而解决了在单处理机环境下程序并发执行问题。此后20年时间里,在多道程序OS中一直以进程为能够拥有资源并独立调度的基本单位。知道20世纪80年代中期,人们才提出了比进程更小的基本单位——线程的概念,并试图用它来提高程序并发执行的程度,以进一步改善系统的服务质量。
线程和进程的关系
- 一个进程可以包含多个线程,一个线程只属于一个进程,线程不能脱离进程而独立运行
- 每一个进程至少包含一个线程(称为主线程);在主线程开始执行程序,Java程序的入口main()方法就是在主线程中被执行的
- 在主线程中可以创建并启动其他线程
- 一个进程内所有的线程共享该进程的内存资源
创建线程
- 继承Thread类的方式
- 在Java中要实现线程,最简单的方式就是扩展Thread类,重写其中的run()方法,方法原型如下:
- Thread类中的run()方法本身并不执行任何操作,如果我们重写了run()方法,当线程启动时,他将执行run()方法
- 定义:
public class MyThread extends Thread{
public void run(){
}
}
- 调用:
MyThread thread = new MyThread();
thread.start();
- 实现Runnable接口的方式
- java.lang.Runnable接口中仅仅只有一个抽象方法:
- public void run();
- 也可以通过实现Runnable接口的方式来实现线程,只需要实现其中的run()方法即可;
- Runnable接口的存在主要是为了解决Java中不允许多继承的问题
- 定义:
public class MyThread implements Runnable{
@Override
public void run(){
....
}
}
- 调用:
MyThread r = new MyThread();
//创建一个线程作为外壳,将r包起来,
Thread thread = new Thread(r);
thread.start()
3.继承方式和实现方式的联系和区别
【区别】
继承Thread类:线程代码存放Thread子类run()方法中。
实现Runnable:线程代码存放在接口子类的run()方法中。
【实现Runnable的好处】
1)避免单继承的局限性
2)多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源
Thread类中的方法
常用方法
方法原型 | 说明 |
---|---|
void start() | 启动线程 |
final String getName() | 返回线程的名称 |
final int setPriority(int newPriority) | 设置线程优先级 |
final int getPriority() | 返回线程优先级 |
final void join() | 等待线程终止 |
static Thread currentThread() | 返回当前正在执行的线程对象的引用 |
final void sleep(long mills) | 让当前正在执行的线程(暂停执行) |
static native void yield() | 线程让步 |
final void wait() | 阻塞当前线程等待唤醒 |
final native void notify() | 唤醒优先级最高的线程 |
下面来大概讲一下比较重要的几个方法:
1.void start()
用start()方法来启动线程,真正实现了多线程运行,这时无需等待run()方法体代码执行完毕就可以执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时线程处于就绪(可运行状态),并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里run()称为线程体,它包含了线程要执行的内容,run()结束,此线程随即终止。
既然讲到了start(),再讲下run()方法,run()方法就是一个普通方法而已,如果直接调用run()方法,程序中仍然只有主线程这一个线程,还是顺序执行,等待run()方法体执行完毕后才可继续执行下面的代码,没有达到写线程的目的。
2.final void join()
用于在某一个线程执行过程中调用另一个线程执行,等待被调用的线程执行结束后,再执行当前进程。
3.final void wait()
Object类中的方法,调用wait()方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify()方法或notifyAll()后本线程才会进入对象锁定池准备获取对象锁进入运行状态
4.final void sleep(long mills)
线程类中的方法,导致此线程暂停执行指定时间,执行机会给其他线程,但是监控状态依旧保持,到时后会自动恢复。调用sleep不会释放对象锁。sleep()使当前线程进入阻塞状态,再指定时间内不会执行。
再来说下sleep()方法和wait()方法的区别和联系
- 两者都可以暂停线程的执行
- 对于sleep()方法,我们知道该方法属于Thread类。而wait()方法,则是属于Object类
- wait()通常用于线程间的交互/通信(可以结合生产者消费者进程理解),sleep()通常用于暂停执行
- sleep()方法导致了程序暂停执行指定的时间,让出CPU给其他线程,但是它的监控状态依旧保持,当指定的时间到了又会自动恢复运行状态
- 在调用sleep()方法过程中,线程不会释放对象锁,而在调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。线程不会自动苏醒
5.final native void notify();
只唤醒一个等待的线程并使该线程开始执行。所以如果有多个线程执行一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。
这是jdk8中的描述
唤醒正在这个对象的监视器上等待的单个线程。如果有任何线程正在等待这个对象,它们中的一个将被唤醒。这种选择是任意的,由实现来决定。
可以参考以下代码:
import java.util.ArrayList;
import java.util.List;
public class Test {
//等待队列
private static List<String> waitList = new ArrayList<String>();
//唤醒队列
private static List<String> notifyList = new ArrayList<>();
private static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
//创建50个线程
for(int i = 0; i < 50; i++){
String threadName = Integer.toString(i);
new Thread(()->{
synchronized (object){
String name = Thread.currentThread().getName();
System.out.println("线程"+name+"正在等待");
waitList.add(name);
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+name+"被唤醒了");
notifyList.add(name);
}
},threadName).start();
Thread.sleep(50);
}
for(int i = 0; i < 50; i++){
synchronized (object){
object.notify();
}
//synchronized是不公平锁,其锁竞争具有随机性,在这里等待为了确保被唤醒的线程获取到对象锁并立即执行
Thread.sleep(50);
}
System.out.println("等待队列"+waitList);
System.out.println("唤醒队列"+notifyList);
}
}
其执行结果为从0-49依次被唤醒
6.static native void yield()
yield方法的作用是放弃当前CPU资源,让给其他任务去执行,放弃时间不确定,有可能刚刚放弃马上获得了CPU时间片,所以在测试时可能会发生执行了yield()方法,但是原来的线程还在执行任务
线程状态
线程有这样几个基本状态:
新建:当一个Thread类或其子类的对象被声明创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已经具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入了运行状态,run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被认为的挂起,或执行输入输出操作,让出CPU并临时终止了自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性终止或出现异常导致结束
下图描述了线程状态之间的转换
守护线程
Java中的线程分为两类:用户线程和守护线程
任何一个守护线程都是整个JVM中所有非守护线程的保姆,只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;
只有当最后一个非守护线程结束时,守护线程会随着JVM一起结束工作。
守护线程的存在是为了其他线程的运行提供便利服务,守护线程最典型的应用就是GC(垃圾回收器),他就是一个很称职的守护者。
用户线程和守护线程两者几乎没有区别,唯一不同之处就是在于虚拟机的离开:如果用户进程已经全部退出运行,只剩下守护线程存在,虚拟机也就退出了。因为没有了被守护者,守护线程也没有工作可做,也没有继续运行程序的必要了。
设置守护线程:setDaemon(boolean on)
注意:设置守护线程必须要在启动线程之前,否则会抛出一个IllegalThreadStateException异常
多线程的概念
多线程是指程序中包含多个执行单元,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行的线程来完成各自的任务。
何时需要多线程?
- 程序需要同时执行两个或多个任务。
- 程序要实现一些需要等待的任务,如用户输入、文件读写、网络操作、搜索等。
多线程的优点
- 提高程序的相应
- 提高CPU的利用率
- 改善程序结构,将复杂任务分为多个线程,独立执行
多线程的缺点
- 线程也是程序,所以线程需要占用内存,线程越多占用内存越多;
- 多线程需要协调管理,所以需要CPU时间跟踪线程;
- 线程之间对共享资源的访问会相互影响
上述缺点第一,第二通过硬件性能可以改善,但是第三个缺点比较致命,可能会导致重大错误。怎样解决请看下面线程同步的介绍
线程同步
多线程同步
- 多个线程同时读写同一份共享资源时,可能会引起冲突。所以引入"线程"同步机制,即各线程间要有先来后到;(和饭堂打饭一样,要是大家都不排队,轻则拥挤不利于群体的打饭效率,而且可能爆发冲突;若是大家都排队,那么拥挤、冲突就可以避免,此时工作人员的打饭效率最高)
同步就是排队+锁: - 几个线程之间要排队,一个一个对共享资源进行操作,而不是同时操作;
- 为了保证数据在方法中被访问的正确性,在访问时加入锁机制(还是打饭的例子,我们在食堂打饭如何保证一个一个进行操作呢?是通过自己前面是否有人来判断打饭阿姨(共享资源)是否被占用,若是被占用,就等待,如果自己前方没有人了,那么就打饭,这样便实现了一个一个进行操作;但是计算机不是人类,它实现是靠代码中的锁是否被占用来进行占用或是等待)
同步锁:
同步锁可以是任意对象,保证多个线程获得的是同一个对象
同步执行过程
1.第一个线程访问,锁定同步对象,执行其中代码
2.第二个线程访问,发现同步对象被锁定,无法访问
3.第一个线程访问完毕,解锁同步对象
4.第二个线程访问,发现同步对象没有锁,然后锁定并访问
在Java中实现同步:
使用synchronized(同步锁)关键字同步方法或代码块
synchronized(同步锁){
//需要同步的代码
}
synchronized还可以放在方法声明中,表示整个方法,为同步方法。
例如:
public synchronized void show (String name){
// 需要被同步的代码;
}
问题:模拟买票,两个窗口分别售票,票数共10张
要求:分别使用继承Thread和实现Runnable两种方式实现
下面是继承Thread类实现买票过程,且用synchronized修饰代码块实现,
下面所有代码为了方便将线程类和main方法写在了一起
public class ThreadTest extends Thread {
static int num = 10;//模拟票的个数
static Object lock = new Object();//必须保证多个线程使用同一个锁对象
//lock的作用记录,是否有线程进入同步代码块中
@Override
public void run() {
while(true){
/*
while(num>0){
synchronized(lock){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String threadName = Thread.currentThread().getName();
System.out.println(threadName+"抢到了"+num--+"张票");
}
}
//为什么不在while()添加num>0条件呢,程序运行了以下,发现总是能买到第0张票,
//这说明在最后一张票被买完且线程放弃对象锁之前,另一个线程早已通过了
//while(num>0)条件的判断,等待对象锁的使用,所以只能在同步代码块内部
//判断是否继续买票
*/
//synchronized写在循环内部,否则,一个线程拿到锁直接将票买完
synchronized (lock){//此时的锁对象是static的对象,在内存中只有一份
if(num>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String threadName = Thread.currentThread().getName();
System.out.println(threadName+"抢到了第"+num--+"张票");
}else{
break;
}
}
}
}
public static void main(String[] args) {
ThreadTest frame1 = new ThreadTest();
frame1.setName("窗口1");
ThreadTest frame2 = new ThreadTest();
frame2.setName("窗口2");
frame1.start();
frame2.start();
}
}
下面是继承Thread类实现买票过程,且用synchronized修饰方法实现
public class ThreadTest extends Thread {
static int num = 10;//模拟票的个数
@Override
public void run() {
while (true){
if(num<=0){
break;
}
printTicket();
}
}
/*
synchronized修饰方法时,
在方法声明位置上没有static关键字,此时锁对象为当前线程对象
如果有static,则此时锁对象为该类的Class类的对象
在这里解释一下类的class类的对象:当一个类被使用时,就会被加载到方法区中,会为每个类创建一个Class类的对象,表示该类的信息
*/
private static synchronized void printTicket() {
if(num>0){//这里必须判断,如果在最后一张票卖出之前另一个线程通过了run()中的判断
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"买到了第"+num--+"张票");
}
}
public static void main(String[] args) {
ThreadTest frame1 = new ThreadTest();
frame1.setName("窗口1");
ThreadTest frame2 = new ThreadTest();
frame2.setName("窗口2");
frame1.start();
frame2.start();
}
}
下面是实现Runnable接口实现买票过程,且用synchronized修饰方法实现
public class ThreadTest implements Runnable {
int num = 10;//多个线程操作一个对象,该变量在内存中只有一份,无需static修饰
@Override
public void run() {
while (true){
if(num>0){
printTicket();
}else{
break;
}
}
}
private synchronized void printTicket() {//没有static只有synchronized修饰,默认锁对象为当前对象,因为main方法创建了一个任务,只是两个线程调用一个任务,所以无需static修饰
if(num>0){//这里必须判断,如果在最后一张票卖出之前另一个线程通过了run()中的判断
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"买到了"+num--+"张票");
}
}
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
Thread thread1 = new Thread(threadTest,"窗口1");
Thread thread2 = new Thread(threadTest,"窗口2");
thread1.start();
thread2.start();
}
}
下面来简单总结一下,继承Thread类和实现Runnable接口两种方式实现多线程同步的操作
其实是继承Thread类还是实现Runable,没有太大区别,主要在于创建了几个任务
先判断创建了几个任务?
如果创建了多个任务,那么共享变量和synchronized修饰的方法(如果有这个方法的话)都需要static修饰;如果没有方法,只有synchronized修饰的代码块,那还需要声明一个静态成员变量作为对象锁,来判断代码块中是否有线程执行。如果synchronized既修饰代码块还修饰方法,那么方法处(synchronized修饰的)既要用static修饰,还要创建一个对象锁
如果创建了一个任务,那么共享变量无需static修饰,且代码块无需创建对象锁,只需this关键字即可,方法处也无需static修饰,因为默认没有static修饰时,对象锁为this,以上代码块和方法都是被synchronized修饰的
以上是我第一次较为完整的学完Java线程同步的总结,如果有错请指出来
Lock
- 从JDK5.0开始,Java提供了更强大的线程同步机制-通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- Java.util.concurrent.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独立访问,每次只有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全控制中,比较常用的是ReentrantLock,可以显示的加锁,释放锁。
这是一段运用了Lock锁的实例,其实就是把synchronized的外壳变为了lock()和unlock()的外壳
public class ThreadTest implements Runnable {
int num = 10;
Lock lock = new ReentrantLock();
//创建ReentrantLock对象,当有线程获取到执行权后,底层state状态由0变为1
//后面如果继续有其他线程访问,都进入一个队列中等待
@Override
public void run() {
while (true){
try{//在try内的代码出现异常也不会影响到锁的释放
lock.lock();//加锁
if(num>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了第"+num--+"张票");
}else{
break;
}
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
Thread thread1 = new Thread(threadTest,"窗口1");
Thread thread2 = new Thread(threadTest,"窗口2");
thread1.start();
thread2.start();
}
}
再来总结一下synchronized锁和Lock锁的区别
1.底层实现原理不同
synchronized是关键字,底层实现不依靠Java代码,依靠于底层指令
Lock锁底层实现,完全依靠Java代码实现
2.用法不同
synchronized可以修饰代码块和方法
而Lock锁只能修饰代码块
3.加锁和释放锁的方式不同
synchronized加锁,释放锁是隐式的
Lock加锁和释放锁都是显式的
死锁:不同的线程分别占有对方需要的共享资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
设计时考虑清楚锁的顺序,尽量减少嵌套的加锁交互数量
运行这段代码程序有概率(在电脑上试了多次,每次都出现死锁)会出现死锁
public class DeadlockTest extends Thread{
//两把锁
static Object obj1 = new Object();
static Object obj2 = new Object();
boolean flag;
public DeadlockTest(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized (obj1){
System.out.println("true obj1");
synchronized (obj2){
System.out.println("true obj2");
}
}
}else{
synchronized (obj2){
System.out.println("false obj2");
synchronized (obj1){
System.out.println("false obj1");
}
}
}
}
public static void main(String[] args) {
DeadlockTest deadlockTest1 = new DeadlockTest(true);
DeadlockTest deadlockTest2 = new DeadlockTest(false);
deadlockTest1.start();
deadlockTest2.start();
}
}
线程通信
线程通讯是指多个线程通过相互牵制,相互调度,即线程间的相互作用
设计三个方法:
.wait一旦执行,当前线程进入阻塞状态,并释放同步监视器
.notify一旦执行,就会唤醒一个被wait的线程,如果由多个线程被wait,那就唤醒优先级最高的那个。上面讲过Thread类中的常用方法,讲过了notify()方法唤醒的顺序,notify函数在jdk中定义为随机唤醒,但是具体实现取决于不同的虚拟机,像主流的hotspot就是使用队列进行维护等待与唤醒的线程,是顺序唤醒的。
.notifyAll一旦执行,就会唤醒所有被wait的线程
例子:两个线程交替打印1-1000的数字
public class PrintNumTest extends Thread{
int num = 1;
@Override
public void run() {
while (true){
synchronized (this){
this.notify();
if(num<=100){
System.out.println(Thread.currentThread().getName()+"打印"+num++);
}else {
break;
}
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
PrintNumTest printNumTest = new PrintNumTest();
Thread thread1 = new Thread(printNumTest,"窗口1");
Thread thread2 = new Thread(printNumTest,"窗口2");
thread1.start();
thread2 .start();
}
}
经典例题:
经典例题:生产者/消费者问题
生产者(Productor)将产品放在柜台(Counter),而消费者(Customer)从柜台
处取走产品,生产者一次只能生产固定数量的产品(比如:1), 这时柜台中不能
再放产品,此时生产者应停止生产等待消费者拿走产品,此时生产者唤醒消费者来
取走产品,消费者拿走产品后,唤醒生产者,消费者开始等待.
测试类
public class Test {
public static void main(String[] args) {
Counter counter = new Counter();
Producter producter = new Producter(counter);
Customer customer = new Customer(counter);
producter.start();
customer.start();
}
}
生产者类
public class Producter extends Thread{
Counter counter;
public Producter(Counter counter){
this.counter = counter;
}
@Override
public void run() {
while (true){
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
counter.add();
}
}
}
消费者类
public class Customer extends Thread{
Counter counter;
public Customer(Counter counter){
this.counter = counter;
}
@Override
public void run() {
while (true){
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
counter.sub();
}
}
}
柜台类
public class Counter {
int num = 0;
public synchronized void sub(){
if(num==0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
num--;
System.out.println("消费");
notify();
}
}
public synchronized void add(){
if(num==0){//产品不够
num++;
System.out.println("生产");
notify();
}else{
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
新增创建线程方式
实现Callable接口与是哟个Runnable接口相比,Callable功能更强大些。
- 相比run方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,获取返回结果
接收任务
Future<Integer> funtureTask = new FutureTask(任务);
创建任务
Thread t = new Thread(futureTask);
t.start();
Integer val = futureTask.get();获取线程call方法的返回值
实现Callable接口的例子:
public class CallableTest implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 1; i < 101; i++){
sum+=i;
}
return sum;
}
public static void main(String[] args) {
CallableTest callableTest = new CallableTest();
FutureTask<Integer> futureTask = new FutureTask<>(callableTest);
Thread thread = new Thread(futureTask);//借助FutureTask类,添加任务到线程中
thread.start();
try {
Integer val = futureTask.get();
System.out.println(val);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
以上所有代码都没有导包,如果有需要的可以自己导包。