线程,看这一篇就够啦

本文详细介绍了Java中的线程和并发编程,包括进程与线程的区别、并发与并行、上下文切换、CAS算法、协程的概念,以及死锁的避免。深入探讨了多线程的使用,如通过继承Thread类、实现Runnable接口和Callable接口创建线程,以及线程的状态和Java并发的三大特性。此外,文章还讲解了synchronized关键字的用法,包括锁的可重入性、锁的优化以及与volatile关键字的区别。最后,介绍了线程通信机制,如wait/notify、管道输入/输出流、Thread.join()以及ThreadLocal的应用。同时,文章提到了Lock接口和ReentrantLock的使用,包括公平锁与非公平锁、读写锁的实现,展示了更高级的并发控制手段。
摘要由CSDN通过智能技术生成

文章目录

多线程

一 进程和多线程

进程

一个程序对应一个进程。程序是静态的,通常存放在外存,当程序运行时,加载到内存中,就形成了进程。

进程是动态的,进程是程序的一次执行过程,是系统运行程序的基本单位。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

线程

线程是进程中独立执行某个任务的单元,一个进程至少有一个线程,一个进程有多个线程就是多线程。

多个线程共享同一块内存空间和一组系统资源,系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,正因为如此,线程也被称为轻量级进程。

并发和并行

并发:同一时刻,只有一个线程运行,cpu以毫秒级的速度在多个线程间切换,宏观上感觉像多个线程同时执行。

并行:同一时刻,多条指令被多个处理机同时执行

上下文切换

线程切换的时候,cpu需要记录当前线程的任务状态,下次切到此线程执行的时候可以根据保存的任务状态继续任务的进度。而保存记录任务状态是需要消耗一定的系统资源。

上下文切换主要分让步式和抢占式。让步式是指执行线程主动释放CPU,通过减少锁竞争和使用CAS算法来避免;抢占式是线程因分配的时间片用尽而被迫放弃CPU或者被其他优先级更高的线程所抢占,通过适当减少线程数和使用协程可以来避免。

注:

  1. 传统操作系统中,cpu操作的基本单位是进程,进程既管理系统资源,又执行cpu分派的任务。操作系统中引入线程后,进程主要管理资源,任务的执行交给线程来执行,从而提高了系统的并发性。
  2. 同一进程中,多个任务并发是多个线程之间的切换,进程不需要切换从而避免了昂贵的系统调用。但是两个进程之间的线程切换依然会进程切换。
  3. 线程的切换同样需要消耗系统资源,这也是线程太多的弊端,多个线程的频繁切换会浪费系统资源(cpu的性能被线程切换消耗)。
  4. 一个Java程序就是一个进程,Java程序一运行,jvm进行内存分配,多个线程共享jvm的堆(heap),每个线程有自己内存空间虚拟机栈(stack)和程序计数器。
  5. 每个进程都是不同的程序,进程通信很困难,每个进程下的多个线程共享堆内存,线程通信相对进程通信要容易的多。
CAS算法

CAS(比较并交换,Compare and swap) 是一种无锁算法,是不使用锁的情况下实现多线程之间的变量同步。

协程

协程可理解为微线程或者说是轻量级的线程,它占用的内存更少并且更灵活。Lua, Ruby语言中都有协程的概念。JAVA可以使用开源协程库:Quasar。

死锁

死锁就是两个线程相互等待,程序不终止也无法执行。

如何避免:

  • 避免一个线程同时获得多个锁
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
  • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
为什么要使用多线程

程序运行加快的原因是运行方式从串行运行变为并发运行,效率会提高。

二 使用多线程

2.1继承Thread类

MyThread.java

    public class MyThread extends Thread {
   
        @Override
        public void run(){
   
            System.out.println("继承Thread启动一个线程");
        }
        //test
        public static void main(String[] args) {
   
            new MyThread().start();
            System.out.println("main finished");
            new MyThread().start();
        }
    }

运行结果:

    main finished
    继承Thread启动一个线程
    继承Thread启动一个线程

从上面的运行结果可以看出:线程是一个子任务,CPU以不确定的方式,或者说是以随机的时间来调用线程中的run方法。

2.2实现Runnable接口

推荐实现Runnable接口方式开发多线程,因为Java单继承但是可以实现多个接口。

MyRunnable.java

public class MyRunnable implements Runnable {
   
    public void run() {
   
        System.out.println("实现Runnable启动一个线程");
    }

    public static void main(String[] args) {
   
        MyRunnable myRunnable = new MyRunnable();
        new Thread(myRunnable).start();
        new Thread(myRunnable).start();
    }
}

运行结果:

    实现Runnable启动一个线程
    实现Runnable启动一个线程
2.3实现Callable接口,callable+futureTask

MyThread3.java

public class MyThread3 implements Callable<String> {
   
    public String call() throws Exception {
   
        return "callable+futureTask,通过future.get()获取";
    }
    public static void main(String[] args) throws Exception{
   
        Callable<String> myThread3=new MyThread3();
        FutureTask<String> futureTask=new FutureTask<String>(myThread3);
        new Thread(futureTask).start();
        //通过futureTask.get()获取结果
        System.out.println(futureTask.get());
    }
}

运行结果

callable+futureTask,通过future.get()获取
2.4实现Callable接口,线程池+future

MyThread4.java

public class MyThread4 {
   
    public static void main(String[] args) throws Exception {
   
        //缓存线程池,4大线程池之一
        ExecutorService threadPool = Executors.newCachedThreadPool();
        Future<String> future = threadPool.submit(new Callable<String>() {
   
            public String call() throws Exception {
   
                return "callable+线程池,通过future.get()获取";
            }
        });
        System.out.println(future.get());
    }
}

运行结果

callable+线程池,通过future.get()获取

三 线程的状态:

新建状态 new
MyThread2 myThread2=new MyThread2(); 

当new一个线程后,该线程处于新建状态。

就绪状态 runnable
new Thread(myThread2).start();

调用start()方法,线程处于就绪状态,该线程加入就绪队列,等待cpu分配时间。

运行状态 running
public void run() {
   
    System.out.println(Thread.currentThread().getName()+"实现Runnable启动一个线程");
}
  1. 线程取得cpu使用权,执行线程体run()方法。cpu分配时间内run()没有执行完毕,线程由运行状态变为就绪状态,等待cpu下次执行。
  2. 线程由运行转为就绪,cpu会记录该线程当前运行状态(上下文)以方便再次切到该线程继续执行任务,多线程的上下文切换会消耗cpu资源。
阻塞状态 blocked
 public static void main(String[] args) {
   
    MyThread myThread=new MyThread();
    myThread.start();
    try {
   
        myThread.sleep(30);
        myThread.join();
        myThread.wait();
    }catch (Exception e){
   
        e.printStackTrace();
    }
}
死亡状态 dead
* 线程体run()方法执行完毕会进入死亡状态。
* 死亡线程是无法start()的。
注: run()和start()的区别是run()方法的方法体是线程的线程执行体,run()代表了线程要执行的操作;start()方法表示启动一个线程,启动一个线程后,线程执行run()方法的内容。

四 java并发的三大特性:

原子性
有序性
指令重排
内存屏障
可见性

volatile解决了有序性和可见性。一个变量用volatile修饰的话,保证了该变量对其他线程可见。

volatile并不保证原子性,这意味着当多个线程修改变量的时候,依然会有并发问题。

AQS:abstractQueuedSynchronizer 抽象队列同步器

五 实例变量和线程安全

定义线程类中的实例变量针对其他线程可以有共享和不共享之分

不共享数据

NoShare.java

public class NoShare extends Thread {
   

    private int count = 5;

    public NoShare(String name) {
   
        super();
        this.setName(name);
    }

    @Override
    public void run() {
   
        super.run();
        while (count > 0) {
   
            count--;
            System.out.println("由 " + Noshare.currentThread().getName()
                    + " 计算,count=" + count);
        }
    }

    public static void main(String[] args) {
   
        NoShare a = new NoShare("A");
        NoShare b = new NoShare("B");
        NoShare c = new NoShare("C");
        a.start();
        b.start();
        c.start();
    }
}

运行结果:

由 B 计算,count=4
由 A 计算,count=4
由 A 计算,count=3
由 A 计算,count=2
由 A 计算,count=1
由 A 计算,count=0
由 B 计算,count=3
由 B 计算,count=2
由 B 计算,count=1
由 B 计算,count=0
由 C 计算,count=4
由 C 计算,count=3
由 C 计算,count=2
由 C 计算,count=1
由 C 计算,count=0

可以看出每个线程都有一个属于自己的实例变量count,它们之间互不影响。

共享数据

Share.java

public class Share extends Thread {
   

    private int count = 5;

    @Override
    public void run() {
   
        super.run();
        count--;
        System.out.println("由 " + Share.currentThread().getName() + " 计算,count=" + count);
    }

    public static void main(String[] args) {
   
        Share share = new Share();
        //下列线程都是通过share对象创建的
        Thread a = new Thread(share, "A");
        Thread b = new Thread(share, "B");
        Thread c = new Thread(share, "C");
        Thread d = new Thread(share, "D");
        Thread e = new Thread(share, "E");
        a.start();
        b.start();
        c.start();
        d.start();
        e.start();
    }
}

运行结果:

由 A 计算,count=4
由 B 计算,count=3
由 C 计算,count=2
由 E 计算,count=1
由 D 计算,count=0

六 线程方法

  • currentThread()
    返回对当前正在执行的线程对象的引用。
  • getId()
    返回此线程的标识符
  • getName()
    返回此线程的名称
  • getPriority()
    返回此线程的优先级
  • isAlive()
    测试这个线程是否还处于活动状态。
    • 什么是活动状态呢?
      活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备运行的状态。*
  • sleep(long millis)
    使当前正在执行的线程以指定的毫秒数“休眠”(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。
  • interrupt()
    中断线程。
    • interrupted():测试当前线程是否已经是中断状态,执行后具有将状态标志清除为false的功能
    • isInterrupted(): 测试线程Thread对相关是否已经是中断状态,但部清楚状态标志
  • setName(String name)
    将此线程的名称更改为等于参数 name 。
  • isDaemon()
    测试这个线程是否是守护线程。
  • setDaemon(boolean on)
    将此线程标记为 daemon线程或用户线程。
  • join()
    在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
    join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行
  • yield()
    yield()方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用CPU时间。注意:放弃的时间不确定,可能一会就会重新获得CPU时间片。
  • setPriority(int newPriority)
    更改此线程的优先级

七 方法使用示例

如何线程终止

使用isInterrupted()+return

TestInterruptUseReturn.java

public class TestInterruptUseReturn extends Thread {
   

    @Override
    public void run() {
   
        while (true) {
   
            if (this.isInterrupted()) {
   
                System.out.println("ֹͣ停止了!");
                return;
            }
            System.out.println("time=" + System.currentTimeMillis());
        }
    }

    public static void main(String[] args) throws InterruptedException {
   
        TestInterruptUseReturn testInterruptUseReturn = new TestInterruptUseReturn();
        testInterruptUseReturn.start();
        Thread.sleep(2000);
        testInterruptUseReturn.interrupt();
    }
}

运行结果:

time=1547013999650
time=1547013999650
time=1547013999650
time=1547013999650
time=1547013999650
time=1547013999650
time=1547013999650
ֹͣ停止了!
如何设置守护线程

可以通过调用Thead类的setDaemon(true)方法设置当前的线程为守护线程

TestDaemon.java:

public class TestDaemon extends Thread {
   
    private int i = 0;

    @Override
    public void run() {
   
        try {
   
            while (true) {
   
                i++;
                System.out.println("i=" + i);
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
   
        try {
   
            TestDaemon testDaemon = new TestDaemon();
            testDaemon.setDaemon(true);
            testDaemon.start();
            Thread.sleep(5000);
            System.out.println("停止!");
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
    }
}

运行结果:

i=42
i=43
i=44
i=45
i=46
i=47
i=48
i=49
i=50
停止!

注意事项

  1. setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException异常
  2. 在守护线程中产生的新线程也是守护线程
  3. 不是所有的任务都可以分配给守护线程来执行,例如读写操作或者计算逻辑

八 线程的优先级

每个线程都具有各自的优先级,线程的优先级可以在程序中表明该线程的重要性,如果有很多线程处于就绪状态,系统会根据优先级来决定首先使哪个线程进入运行状态。但这个并不意味着低 优先级的线程得不到运行,而只是它运行的几率比较小,垃圾回收机制线程的优先级就比较低,所以很多垃圾得不到及时的回收处理。

  • 线程优先级具有继承特性假如A线程启动B线程,则B线程的优先级和A是一样的。
  • 线程优先级具有随机性也就是说线程优先级高的不一定每一次都先执行完。

Thread类中包含的成员变量代表了线程的某些优先级。如Thread.MIN_PRIORITY(常数1),Thread.NORM_PRIORITY(常数5), Thread.MAX_PRIORITY(常数10)。其中每个线程的优先级都在Thread.MIN_PRIORITY(常数1) 到Thread.MAX_PRIORITY(常数10) 之间,在默认情况下优先级都是Thread.NORM_PRIORITY(常数5)。

TestPriorityFather.java:

public class TestPriorityFather extends Thread {
   
    @Override
    public void run() {
   
        System.out.println("Father run priority=" + this.getPriority());
        TestPrioritySon testPrioritySon = new TestPrioritySon();
        testPrioritySon.start();
    }
}

TestPrioritySon.java

public class TestPrioritySon extends Thread {
   

    @Override
    public void run() {
   
        System.out.println("son run priority=" + this.getPriority());
    }

    public static void main(String[] args) {
   
        System.out.println("main thread begin priority="
                + Thread.currentThread().getPriority());
        Thread.currentThread().setPriority(4);
        System.out.println("main thread end   priority="
                + Thread.currentThread().getPriority());
        TestPriorityFather testPriorityFather=new TestPriorityFather();
        testPriorityFather.start();
    }
}

运行结果:

main thread begin priority=5
main thread end   priority=4
Father run priority=4
son run priority=4

synchronized

一 简介

synchronized是一种独占锁,也是悲观锁。

在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后变得在某些情况下并不是那么重了。

二 synchronized锁多个对象

先看例子:

HasSelfPrivateNum.java

public class HasSelfPrivateNum {
   

    private int num = 0;

    public synchronized void addName(String username) {
   
        try {
   
            if (username.equals("a")) {
   
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);//如果去掉thread.sleep(2000),那么运行结果就会显示为同步的效果
            } else {
   
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(username + " num=" + num);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
   
        HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
        HasSelfPrivateNum hasSelfPrivateNum1 = new HasSelfPrivateNum();
        ThreadA threadA = new ThreadA(hasSelfPrivateNum);
        ThreadB threadB = new ThreadB(hasSelfPrivateNum1);
        threadA.start();
        threadB.start();
    }
}

ThreadA.java

public class ThreadA extends Thread {
   
    private HasSelfPrivateNum hasSelfPrivateNum;

    public ThreadA(HasSelfPrivateNum hasSelfPrivateNum) {
   
        super();
        this.hasSelfPrivateNum = hasSelfPrivateNum;
    }

    @Override
    public void run() {
   
        hasSelfPrivateNum.addName("a");
    }
}

ThreadB.java

public class ThreadB extends Thread {
   

    private HasSelfPrivateNum hasSelfPrivateNum;

    public ThreadB(HasSelfPrivateNum hasSelfPrivateNum) {
   
        this.hasSelfPrivateNum = hasSelfPrivateNum;
    }

    @Override
    public void run() {
   
        hasSelfPrivateNum.addName("b");
    }
}

运行结果:

a set over!
b set over!
b num=200
a num=100

a num=100停顿一会才执行

因为synchronized锁的对象,哪个线程先执行带synchronized关键字的方法,就持有该方法所属对象的锁,其他线程只能等待,多个线程访问的是同一个对象。以上代码中创建了两个HasSelfPrivateNum类对象,所以就产生了两个锁。

三 脏读

发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过。

PublicFiled.java

public class PublicFiled {
   

    public String username = "A";

    public String password = "AA";

    public synchronized void setValue(String username, String password) {
   

        try {
   
            this.username = username;
            Thread.sleep(5000);
            this.password = password;
            System.out.println("setValue method thread name=" + Thread.currentThread().getName() + " username=" + username + " password=" + password);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
    }

    //该方法前加上synchronized关键字就同步了
    public void getValue() {
   
        System.out.println("getValue method thread name=" + Thread.currentThread().getName() + " username=" + username + " password=" + password);
    }

    public static void main
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值