Java线程(从基本概念到线程安全,超详细加大量代码实现)

线程

线程基本概念

  • 一个线程是一个程序内部的顺序控制流

  • 线程和进程

    • 每个进程都有独立的代码和数据空间(进程上下文),进程切换的开销大
    • 线程:轻量的线程,同一类线程共享和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小
  • 多进程:在操作系统中,能同时运行多个任务(程序)

  • 多线程:在同一应用程序中,有多个顺序流同时执行

线程的概念模型

  • 虚拟的CPU,封装在Java.lang.Thread类中
  • CPU所执行的代码,传递给Thread类
  • CPU所处理的数据,传递给Thread类

线程体

  • Java的线程是通过java.lang.Thread类来是实现的。
  • 每个线程都是通过某个特定Thread对象的方法run()来完成其操作的,方法run()称为线程体

构造线程的两个方法

  • 定义一个线程类,它继承类Thread并重写其中的方法run();

  • 提供一个实现接口Runnable的类作为线程的目标对象,在初始化一个Thread类或者Thread子类的线程对象时,把目标对象传递给这个线程实例,由该目标对象提供线程体run()。

    • public Thread(ThreadGroup group, Runnable target, String name);

创建一个线程

继承Thread类–创建多线程的方法之一

  • 从Thread类派生一个子类,并创建子类的对象
  • 子类应该重写Thread类的run方法,写入需要在新线程中执行的语句段
  • 调用strat方法来启动新线程,自动进入run方法

创建线程的实例

public class FactorialThread extends Thread{ //继承Thread类
    private int num;
    public FactorialThread(int num)
    {
        this.num = num;
    }
    public void run() //重写run方法
    {
        int i=num;
        int result=1;
        System.out.println("New Thread Started!");
        while(i>0)
        {
            result*=i;
            i--;
        }
        System.out.println("The Factorial of "+num+" is "+result);
        System.out.println("New Thread Ends");
    }
    public static void main(String[] args) {
        FactorialThread f = new FactorialThread(3);
        f.start(); //通过调用start方法启动线程
        System.out.println("main thread ends!");
    }
}

输出结果为

main thread starts
main thread ends!
New Thread Started!
The Factorial of 3 is 6
New Thread Ends

线程的休眠

我们先修改前面的程序,让主线程main休眠一毫秒。

public class FactorialThread extends Thread{
    private int num;
    public FactorialThread(int num)
    {
        this.num = num;
    }
    public void run()
    {
        int i=num;
        int result=1;
        System.out.println("New Thread Started!");
        while(i>0)
        {
            result*=i;
            i--;
        }
        System.out.println("The Factorial of "+num+" is "+result);
        System.out.println("New Thread Ends");
    }
    public static void main(String[] args) {
        System.out.println("main thread starts");
        FactorialThread f = new FactorialThread(10);
        f.start();
        try 
        {
            Thread.sleep(1);
        } 
        catch (Exception e) {}	
        System.out.println("main Thread ends");
    }
}

输出结果为

main thread starts
New Thread Started!
The Factorial of 10 is 3628800
New Thread Ends
main Thread ends

可以看到,一个线程的休眠,使得其他线程得以运行

可以试着运行这个程序,观察结果

public class ThreadTester extends Thread{

    private int SleepTime;
    public ThreadTester(String name)
    {
        super(name);
        SleepTime = (int)(Math.random()*6000); //取一个随机随眠时间
    }
    public void run()
    {
        try {
            System.out.println(
                getName()+"Going to sleep for "+SleepTime);
            Thread.sleep(SleepTime);
        } catch (InterruptedException e) {}	
        System.out.println(getName()+"finished");
    }
}

//另一个类

public class ThreadSleepTester {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ThreadTester thread1 = new ThreadTester("thread1");
        ThreadTester thread2 = new ThreadTester("thread2");
        ThreadTester thread3 = new ThreadTester("thread3");
        System.out.println("Starting threads");
        thread1.start();
        thread2.start();
        thread3.start();
        System.out.println("Threads Started, main ends\n");
    }
}

通过Runnable接口创建线程

Runnable接口

  • 只有一个run()方法
  • Thread类实现了Runnable接口
  • 便于多个线程共享资源
  • Java不支持多继承,如果已经继承了某个基类,便需要实现Runnable接口来生成多线程
  • 以实现Runnable的对象为参数建立新的线程
  • start方法启动线程就会运行run()方法

修改前面的代码

public class FactorialThread implements Runnable{ //实现Runnable接口
    private int num;
    public FactorialThread(int num)
    {
        this.num = num;
    }
    public void run()
    {
        int i=num;
        int result=1;
        System.out.println("New Thread Started!");
        while(i>0)
        {
            result*=i;
            i--;
        }
        System.out.println("The Factorial of "+num+" is "+result);
        System.out.println("New Thread Ends");
    }
    public static void main(String[] args) {
        System.out.println("main thread starts");
        FactorialThread f = new FactorialThread(10);
        new Thread(f).start(); //以实现Runnable的对象为参数建立新的线程
        try 
        {
            Thread.sleep(1);
        } 
        catch (Exception e) {}	
        System.out.println("main Thread ends");
    }
}

同样的,我们修改前面另外一个程序的代码

public class ThreadTester implements Runnable{
    private int SleepTime;
    public ThreadTester()
    {
        SleepTime = (int)(Math.random()*6000);
    }
    public void run()
    {
        try {
            System.out.println(
                Thread.currentThread().getName()+"Going to sleep for "+SleepTime);
            Thread.sleep(SleepTime);
        } catch (InterruptedException e) {}	
        System.out.println(Thread.currentThread().getName()+"finished");
    }
}

测试类

public class ThreadSleepTester {
    public static void main(String[] args) {
        ThreadTester thread1 = new ThreadTester();
        ThreadTester thread2 = new ThreadTester();
        ThreadTester thread3 = new ThreadTester();
        System.out.println("Starting threads");
        new Thread(thread1,"Thread1").start();
        new Thread(thread2,"Thread2").start();
        new Thread(thread3,"Thread3").start();
        System.out.println("Threads Started, main ends\n");
    }
}

我们看一下前面的代码,如果我们修改成这样会出现什么?

public class ThreadSleepTester {
    public static void main(String[] args) {
        ThreadTester thread1 = new ThreadTester();

        System.out.println("Starting threads");
        new Thread(thread1,"Thread1").start();
        new Thread(thread1,"Thread1").start();
        new Thread(thread1,"Thread1").start();
        System.out.println("Threads Started, main ends\n");
    }
}

输出结果

Starting threads
Threads Started, main ends

Thread1Going to sleep for 3389
Thread1Going to sleep for 3389
Thread1Going to sleep for 3389
Thread1finished
Thread1finished
Thread1finished

可以看到,通过Runnable来构造我们的线程体,这些线程是数据共享的

我们看一个数据共享的例子

使用线程模拟三个售票机出售200张票

public class SellTickets implements Runnable{
    private int tickets=200;
    public void run()
    {
        while(tickets>0)
        {
            System.out.println(
                Thread.currentThread().getName()+
                " is selling ticket "+tickets--);
        }
    }
}

测试类

public class SellTicketsTester {
    public static void main(String[] args) {
        SellTickets t = new SellTickets();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}

输出结果

Thread-1 is selling ticket 200
Thread-1 is selling ticket 197
Thread-2 is selling ticket 198
Thread-2 is selling ticket 195
Thread-2 is selling ticket 194
...

多线程的同步控制

有时线程之间彼此不独立、需要同步

  • 线程的互斥

关于线程互斥可以看这里的内容,操作系统进程锁机制

  • 用两个线程模拟存票、售票过程

    • 假定开始售票处并没有票,一个线程往里存票,另外一个线程则往外出售票
    • 我们新建一个票类对象,让存票和售票线程都访问它。本例采用两个线程共享一个数据对象来实现对同一份数据的操作

我们实现这个代码的关键之处就是两个线程对这个票的修改操作一定不能同时进行,不然就乱套了

测试类

public class ProducerAndConsumer {
    public static void main(String[] args) {
        Tickets t = new Tickets(10);
        new Consumer(t).start();
        new Producer(t).start();	
    }
}

票类

public class Tickets {
    int number = 0; //票号
    int size; //票的最高数量
    boolean available = false; //表示是否有票可出售
    public Tickets(int size) //构造函数初始化
    {
        this.size = size;
    }
}

生产者,也就是存票机

public class Producer extends Thread{
    Tickets t = null; 
    public Producer(Tickets t)
    {
        this.t = t;
    }
    public void run()
    {
        while(t.number < t.size) //当票号小于总数,开始存票
        {
            System.out.println("Producer puts ticket "
                    + (++t.number));
            t.available = true;
        }
    }
}

消费者,也就是售票机

public class Consumer extends Thread{
    Tickets t = null;
    int i=0;
    public Consumer(Tickets t)
    {
        this.t = t;
    }
    public void run()
    {
        while(i<t.size)
        {
            if(t.available==true && i<=t.number) //当有票可出售
                System.out.println("Consumer buys tickets "+(++i));
            if(i==t.number)
            {
                t.available = false;
            }
        }
    }
}

输出结果

Producer puts ticket 1
Consumer buys tickets 1
Producer puts ticket 2
Consumer buys tickets 2
Producer puts ticket 3
Consumer buys tickets 3
Producer puts ticket 4
Consumer buys tickets 4
Producer puts ticket 5
Consumer buys tickets 5
Producer puts ticket 6
Consumer buys tickets 6
Producer puts ticket 7
Consumer buys tickets 7
Producer puts ticket 8
Producer puts ticket 9
Consumer buys tickets 8
Consumer buys tickets 9
Producer puts ticket 10
Consumer buys tickets 10

以上结果是不唯一的,有各种各样的可能

如果我们修改一下代码

if(i==t.number)
{
    try{Thread.sleep(1);}
    catch(InterruptedException exception) {}
    t.available = false;
}

看一下输出结果

在这里插入图片描述

直接就死循环了

为什么?

if(i==t.number)
{
    try{Thread.sleep(1);}
    catch(InterruptedException exception) {}
    t.available = false;
}

当售票机在将t.available置成false之前,休眠了一毫秒,导致生产者进程先执行完,然后没有线程再将aviliable置成true,因此消费者线程就陷入了死循环

前面的代码其实是有问题的

我们看一个输出结果

Producer puts ticket 1
Producer puts ticket 2
Consumer buys tickets 1
Producer puts ticket 3
Consumer buys tickets 2
Producer puts ticket 4
Consumer buys tickets 3
Producer puts ticket 5
Consumer buys tickets 4
Consumer buys tickets 5
Consumer buys tickets 6
Producer puts ticket 6
Producer puts ticket 7
Consumer buys tickets 7
Consumer buys tickets 8
Producer puts ticket 8
Producer puts ticket 9
Producer puts ticket 10
Consumer buys tickets 9
Consumer buys tickets 10

可以看到,生产者还没生产出6号票,但是消费者却消费了6号票,为什么会这样?

其实这就是生产者消费者的对Tickets这个类的修改没有实现互斥,两个线程同时修改Tickets肯定会出错,可以参照我前面的那篇文章

  • 互斥:许多线程在同一个共享数据上操作而互不干扰,同一时刻只能有一个线程访问该共享数据,因此有些方法或程序段在同一时刻只能被一个线程执行,称之为监视区(OS里边称为临界区)
  • 协作:多个线程可以有条件地同时操作共享数据。执行监视区代码的线程在条件满足的情况下可以允许其他线程进入监视区

如何实现互斥与协作?

  • synchronized – 线程同步关键字,实现互斥

    • 用于指定需要同步的代码段或方法,也就是监视区

    • 可实现与一个锁的交互。例如

      • synchronized(对象){代码段}
    • synchronized的功能是:首先判断对象的锁是否存在,如果在就获得锁,然后就可以执行紧随其后的代码段;如果对象的锁不在(已被其他的线程拿走),就进入等待状态,直到获得锁

    • 当被synchronized限定的代码段执行完,就释放锁

现在,我们利用锁机制来重写前面的代码,让存票过程和售票过程互斥,即不能同时进行

售票过程和存票过程都是对Tickets对象的修改,而一个对象只有一个锁,我们就利用多进程对锁的争夺实现线程的互斥

代码实现

Consumer类

public class Consumer extends Thread{
    Tickets t = null;
    int i=0;
    public Consumer(Tickets t)
    {
        this.t = t;
    }
    public void run()
    {
        while(i<t.size)
        {
            synchronized(t) //获得锁
            {
                if(t.available==true && i<=t.number)
                    System.out.println("Consumer buys tickets "+(++i));
                if(i==t.number)
                {
                    try {Thread.sleep(1);}
                    catch(Exception e){}
                    t.available = false;
                }	
            }//释放锁,synchronized里面的操作为原子操作,一定是一次全执行完
        }
        System.out.println("Consumer ends");
    }
}

Producer类

public class Producer extends Thread{
    Tickets t = null;
    public Producer(Tickets t)
    {
        this.t = t;
    }
    public void run()
    {
        while(t.number < t.size)
        {
            synchronized(t)//获得锁
            {
                System.out.println("Producer puts ticket "
                    + (++t.number));
                t.available = true;
            }//释放锁
        }
        System.out.println("Producer ends");
    }    
}

输出结果:

Producer puts ticket 1
Consumer buys tickets 1
Producer puts ticket 2
Producer puts ticket 3
Producer puts ticket 4
Producer puts ticket 5
Consumer buys tickets 2
Consumer buys tickets 3
Consumer buys tickets 4
Consumer buys tickets 5
Producer puts ticket 6
Producer puts ticket 7
Producer puts ticket 8
Producer puts ticket 9
Producer puts ticket 10
Producer ends
Consumer buys tickets 6
Consumer buys tickets 7
Consumer buys tickets 8
Consumer buys tickets 9
Consumer buys tickets 10
Consumer ends

可以多运行几次测试一下,不会再出现消费者“超前消费”的现象

**说明:**当线程通过synchronized得到不到对象的锁时,就会被放到对应的等待线程池里边,后面的线程调度算法就是研究应该从线程池里取出拿一个线程来执行

可以通过synchronized关键字定义整个方法在同步控制下执行

修改一下上面的代码

public class Tickets {
    int number = 0;
    int size;
    int i=0;
    boolean available = false;
    public Tickets(int size)
    {
        this.size = size;
    }
    public synchronized void put()
    {
        System.out.println("Producer puts ticket "
                + (++number));
        available = true;
    }
    public synchronized void buy()
    {
        if(available==true && i<=number)
            System.out.println("Consumer buys ticket"+(++i));
        if(i==number)
            available=false;
    }
}

public class Producer extends Thread{
    Tickets t = null;
    public Producer(Tickets t)
    {
        this.t = t;
    }
    public void run()
    {
        while(t.number<t.size)
            t.put();
    }
    
}

public class Consumer extends Thread{
    Tickets t = null;
    int i=0;
    public Consumer(Tickets t)
    {
        this.t = t;
    }
    public void run()
    {
        while(t.i<t.size)
            t.buy();
    }
}

同步与锁的要点

  • 只能同步方法,而不能同步变量
  • 每个对象只有一个锁,当提到同步时,应该清楚在上面上同步?也就是说,在哪个对象上同步?
  • 类可以同时拥有同步方法和非同步方法,非同步方法可以被多个线程自由访问而不受锁的限制
  • 如果两个线程使用相同的实例来调用的synchronized方法,那么一次只能有一个线程执行方法,另一个线程需要等待锁
  • 线程休眠时,并不会释放它所持有的锁
  • 线程可以获得多个锁,比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁
  • 同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块

线程的等待与呼唤

线程的等待–wait()方法:wait方法使线程进入等待状态并释放对象X的锁,这时它需要其它线程在对象X上调用notifynotifyAll方法来唤醒它,这一过程体现了线程之间的沟通

线程的唤醒–notify()和notifyAll()方法

  • notify()随机唤醒一个等待的线程,本线程继续执行

    • 线程被唤醒后,还要等待发出唤醒消息者释放监视区,这期间关键数据仍可能被改变
    • 被唤醒的线程开始执行时,一定要判断当前状态释放合适自己运行
  • notifyAll()唤醒所有等待的线程,本线程继续执行

我们修改一下前面的售票机存票机工作原理

现在要求存票机存一张售票机就卖一张

代码实现:

public class Tickets {
    int number = 0;
    int size;
    int i=0;
    boolean available = false;
    public Tickets(int size)
    {
        this.size = size;
    }
    public synchronized void put()
    {
        if(available==true)
            try {wait();} //如果票还没卖出去,就等待
            catch (InterruptedException e) {}
        System.out.println("Producer puts ticket "
                + (++number));
        available = true;
        notify(); //唤醒线程
    }
    public synchronized void buy()
    {
        if(available==false)
            try {wait();} //如果没票就等待
            catch (InterruptedException e) {}
        System.out.println("Consumer buys tickets "+number);
        available=false;
        notify(); //唤醒线程
        if(number==size) //给个退出条件
            number=size+1;
    }
}

public class Producer extends Thread{
    Tickets t = null;
    public Producer(Tickets t)
    {
        this.t = t;
    }
    public void run()
    {
        while(t.number<t.size)
            t.put();
    }
}

public class Consumer extends Thread{
    Tickets t = null;
    public Consumer(Tickets t)
    {
        this.t = t;
    }
    public void run()
    {
        while(t.number<t.size)
            t.buy();
    }
}

测试类跟前面的一样,这里就不重新贴上去了

后台线程

  • 后台线程

    • 也叫守护线程,通常是为了辅助其他线程而运行的线程
    • 它不妨碍程序终止
    • 一个进程中只要还有一个前台程序在运行,这个进程就不会结束;如果一个进程中的所有前台线程都已经结束,那么无论是否还有未结束的后台线程,这个进程都会结束
    • “垃圾回收”便是一个后台线程
    • 如果对某个线程对象在启动(调用start方法)之前调用了setDaemon(true)方法,这个线程就变成了后台线程

创建一个无限循环的后台线程,验证主线程结束后,程序即结束

public class Ex8_10 {

    public static void main(String[] args) {
        ThreadTest t = new ThreadTest();
        t.setDaemon(true);
        t.start();
    }
}

public class ThreadTest extends Thread{
    public void run()
    {
        while(true)
        {}
    }
}

如果注释掉t.setDaemon(true),那么程序将永不停止

线程的生命周期

NEKLa4.png

简要说明一下死锁问题,有关死锁的更详细内容可看这篇博客进程同步与信号量以及死锁处理

死锁表示一个线程占有着一个资源,但是它需要得到另外一个资源才能完成工作并释放资源,这时,如果它需要得到的另外一个资源在另外一个线程上,同时这个线程跟它处于一样的情况,当多个这样的线程围成一个环,就出现了死锁问题,谁也不释放资源同时谁也得不到资源

简单的死锁避免方法

假设有这样一个游戏,三个人站在三角形的三个顶点,三条边上有三个球,每个人必须先拿到左边的球才能再拿到右手边的球,两手都有球之后,才能把两个球都放下

如图所示:

在这里插入图片描述

有没有发现,跟前面的死锁问题很像,如果每个人左手都拿着球,那每个人都没办法拿到右手边的球,那他们永远没办法把球放下,这就是死锁问题,我们用代码实现一下这个过程

public class Ex8_11 {

    public static void main(String[] args) {
        Balls ball = new Balls();
        Player0 p0 = new Player0(ball);
        Player1 p1 = new Player1(ball);
        Player2 p2 = new Player2(ball);
        p0.start();
        p1.start();
        p2.start();
    }
    
}
class Balls{
    boolean flag0 = false;
    boolean flag1 = false;
    boolean flag2 = false;
}
class Player0 extends Thread{
    private Balls ball;
    public Player0(Balls b)
    {
        this.ball = b;
    }
    public void run()
    {
        while(true)
        {
            while(ball.flag1 == true) {};
            //如果左手边球有人持着,就进入死循环等待
            ball.flag1 = true;
            //获得左手球
            while(ball.flag0 == true) {};
            //右手球有人持着,就进入死循环等待
            if(ball.flag1==true && ball.flag0==false)
            {
                ball.flag0 = true;
                //持右手球
                System.out.println("Player0 has got two balls!");
                ball.flag1 = false;
                ball.flag0 = false;
                //释放左右手球
                try {sleep(1);}
                catch(Exception e){}
                //休眠1毫秒
            }
        }
    }
}
class Player1 extends Thread{
    private Balls ball;
    public Player1(Balls b)
    {
        this.ball = b;
    }
    public void run()
    {
        while(true)
        {
            while(ball.flag0 == true) {};
            ball.flag0 = true;
            while(ball.flag2 == true) {};
            if(ball.flag0==true && ball.flag2==false)
            {
                ball.flag2 = true;
                System.out.println("Player1 has got two balls!");
                ball.flag0 = false;
                ball.flag2 = false;
                try {sleep(1);}
                catch(Exception e){}
            }
        }
    }
}
class Player2 extends Thread{
    private Balls ball;
    public Player2(Balls b)
    {
        this.ball = b;
    }
    public void run()
    {
        while(true)
        {
            while(ball.flag2 == true) {};
            ball.flag2 = true;
            while(ball.flag1 == true) {};
            if(ball.flag2==true && ball.flag1==false)
            {
                ball.flag1 = true;
                System.out.println("Player3 has got two balls!");
                ball.flag2 = false;
                ball.flag1 = false;
                try {sleep(1);}
                catch(Exception e){}
            }
        }
    }
}

运行一段时间之后,你就会发现进入了程序不会再输出任何信息,一个每一个玩家都没办法得到两个球,进入死锁状态

如果想避免死锁,可以修改规则,比如说,如果手持左手球时,右手球不能得到,就将左手球放下

线程的调度

线程调度的要点如下

线程的调度就是CPU维护线程的队列,应该将哪个线程放在队列前面,哪个线程放在队列后面

Java虚拟机使用简单的优先级方法,优先级高的线程先执行

关于线程优先级,一般线程优先级初始化为5,在1-10之间

一个线程运行过程中创建的另一个线程,被创建的线程优先级与创建它的线程相同

线程创建了,可以通过setPriority(int priority)方法修改线程的优先级

优先级高的线程比优先级低的线程先执行

相同优先级的线程,Java的处理方式为随机选择一个线程来执行

线程的优先级应该是基于效率来考虑的,而不是用来保证算法的正确性

一个正在运行的线程只有在以下情况才会停止

  1. 优先级比它高的线程处于就绪状态
  2. 自身调用wait(),sleep,yield()方法
  3. 如果是支持时间片的操作系统,时间片用完也会停止

关于操作系统线程调度可以看这篇文章CPU调度策略以及linux0.11中的实现

我们用代码来观察以下Java线程调度

public class Ex8_13 {

    public static void main(String[] args) {
        TestThread[] runners = new TestThread[2];
        for(int i=0; i<2; i++)
            runners[i] = new TestThread(i);
        runners[0].setPriority(2); //设置优先级
        runners[1].setPriority(3);
        for(int i=0; i<2; i++)
            runners[i].start();
    }
}
class TestThread extends Thread{
    private int tick=1;
    private int num;
    public TestThread(int i) {this.num=i;}
    public void run()
    {
        while(tick<400000)
        {
            tick++;
            if((tick%50000)==0)
            {
                System.out.println("Thread#"+num+",tick="+tick);
                yield(); //让出CPU给同优先级的进程执行
            }
        }
    }
}

如果你的电脑是单CPU的,那么你就会发现,只有等线程1执行完之后,线程0才能执行,但是如果你电脑是多CPU的,这代码就没有多大的参考价值了

但是我们依然可以得到下列几点

一个线程调用yield()方法,只有优先级相同跟这个线程相同才会被调度

但是一个线程如果调用sleep,wait等方法,则所有线程都可以来争夺CPU,而Java会优先让优先级高的线程执行

线程安全与线程兼容与对立

线程安全

线程安全不是指这个线程是否安全,而是指一个对象在多线程的情况下,不使用同步等措施,各个线程调用这个对象都能得到正确的结果,那么这个对象就是线程安全的

Java的线程安全

不可边:既然这个对象不能被修改,那它肯定就是线程安全了

  • final修饰
  • java.lang.String:String s =“String”;
  • 枚举类型:public enum Color{RED,GREEN,BLANK,YELLOW}
  • java.lang.Number的子类如Long,Double
  • BigInteger,BigDecimal(数值类型的高精度实现)

绝对线程安全

  • 满足Brian Goetz在《Java Concurrency In Practice》中定义的线程为绝对线程安全
  • Java API中标注自己是线程安全的类绝大部分不是绝对线程安全(如:Java.util.Vector),如果想达到绝对线程安全,还要在外部加上一些同步

相对线程安全

  • 通常意义上是线程安全,需要保证这个对象单独操作是线程安全的,调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就需要在调用时使用同步手段保证调用的正确性
  • 如:Vector,HashTable等

我们看一个Vector的例子

import java.util.Vector;

public class VectorSafe {
    private static Vector<Integer>vector=new Vector<Integer>();
    public static void main(String[] args) {
        while(true)
        {
            for(int i=0; i<10; i++)
                vector.add(i);
            Thread removeThread = new Thread(new Runnable(){
                public void run()
                {
                    for(int i=0; i<vector.size();i++)
                        vector.remove(i);
                }
            }) ;
            Thread printThread = new Thread(new Runnable(){
                public void run()
                {
                    for(int i=0; i<vector.size(); i++)
                        System.out.println(vector.get(i));
                }
            });
            //上面之间使用内部类的方法来创建线程
            removeThread.start();
            printThread.start();
            while(Thread.activeCount()>20);
            //保证程序中的线程数量不超过20
        }
    }
}

在运行的时候,你会发现,有时候会突然闪过一个错误,数组越界的错误,因为removeThreadprintThread两个线程同时对vector对象操作,有可能一个线程刚删除,另外一个线程就去访问被删除的元素,所以就造成了数组越界,因此Vector并不是绝对线程安全,我们可以给它加上一些同步使它变成线程安全

import java.util.Vector;

public class VectorSafe {
    private static Vector<Integer>vector=new Vector<Integer>();
    public static void main(String[] args) {
        while(true)
        {
            for(int i=0; i<10; i++)
                vector.add(i);
            Thread removeThread = new Thread(new Runnable(){
                public void run()
                {
                    synchronized(vector)
                    {
                        for(int i=0; i<vector.size();i++)
                        vector.remove(i);
                    }
                }
            }) ;
            Thread printThread = new Thread(new Runnable(){
                public void run()
                {
                    synchronized(vector)
                    {
                        for(int i=0; i<vector.size(); i++)
                            System.out.println(vector.get(i));
                    }
                }
            });
            removeThread.start();
            printThread.start();
            while(Thread.activeCount()>2);
        }
    }
}

现在就不会出现数组越界的错误了

线程兼容

对象本身不是线程安全的,但是可以在调用端正确的使用同步手段来保证对象在并发环境中可以安全使用

线程对立

无论调用端是否采取了同步手段,它都无法在多线程被安全使用

线程的安全实现

互斥同步

互斥同步: 实现互斥同步有几种方法,临界区, 信号量,互斥量

不了解信号量,临界区可以看这篇文章进程同步与信号量以及死锁处理

在Java中实现互斥同步有两种方法

  • Synchronized关键字:经过编译后,会在同步块前后形成monitorenter和monitorexit两个字节码。

    • synchronized同步块对自己是可重入的,不会将自己锁死
    • 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入

Synchronized我们前面用过很多次了,这里就不举例了

  • 重入锁ReentrantLock (Java.util.concurrent)
  • 相比采用synchronized,重入锁可实现:等待可中断、公平锁、锁可以绑定多个条件

我们修改前面的程序,将synchronized换成ReentrantLock来实现

    import java.util.Vector;
    import java.util.concurrent.locks.ReentrantLock;

    public class VectorSafe {
        private static Vector<Integer>vector=new Vector<Integer>();
        private static ReentrantLock lock = new ReentrantLock();
        public static void main(String[] args) {
            while(true)
            {
                for(int i=0; i<10; i++)
                    vector.add(i);
                Thread removeThread = new Thread(new Runnable(){
                    public void run()
                    {
                        lock.lock();
                        try
                        {
                            for(int i=0; i<vector.size();i++)
                                vector.remove(i);
                        }
                        finally
                        {
                            lock.unlock();
                        }
                    }
                });
                Thread printThread = new Thread(new Runnable(){
                    public void run()
                    {
                        lock.lock();
                        try
                        {
                            for(int i=0; i<vector.size(); i++)
                                System.out.println(vector.get(i));
                        }
                        finally
                        {
                            lock.unlock();
                        }
                    }
                });
                removeThread.start();
                printThread.start();
                while(Thread.activeCount()>20);
            }
        }
    }

上面的例子只是简单的介绍ReentrantLocK的使用方法

ReentrantLock效率要高于synchronized

非阻塞同步

前面使用的互斥同步,是一种较悲观的处理方式,它认为一定会出现多个线程同时处理这个对象,因此为了防止这种情况发生,它一次只允许一个线程访问对象,其他线程都要进入阻塞状态

而非阻塞同步就是一种较乐观的处理方式,它不让线程进入阻塞,当一个线程访问对象不能得到正确的结果,它就不断地重试,直到获得正确输出

如何使用非阻塞同步?

  • 使用硬件处理器指令进行不断重试策略(JDK1.5以后)

    • 测试并设置(Test-and-Set)
    • 获取并增加(Fetch-and-Increment)
    • 交换(Swap)
    • 比较并交换(Compare-and-Swap,简称CAS)
    • 加载连接,条件存储(Load-Linked,Store-conditional,简称LL,SC)
      例:java实现类AtomicInteger,AtomicDouble等等

也就是说,如果我们想要使用非阻塞同步,应该使用java已经实现非阻塞同步的类,因为其是直接使用硬件实现的,才能支撑不断重试所损耗的效率

举个例子

Class Counter
{
    private volatile int count=0;
    public synchronized void increment()
    {
        count++;
    }
    public int getCount()
    {
        return count;
    }
}

这是阻塞同步,我们将其变成非阻塞同步

class Counter
{
    private AtomicInteger count = new AtomicInteger();
    public void increment()
    {
        count.incrementAndGet();
    }
    public in getCount()
    {
        return count.get();
    }
}

使用AtomicInteger类的话不用使用阻塞同步依然能够实现线程安全

无同步方案

  • 可重入代码:在代码执行时,CPU切换到另一个线程去执行,回来时该代码依然能正确执行,则称该代码为可重入代码
  • 线程本地存储:如果一个代码所需要的数据必须与其他代码共享,而这些代码能保证在一个线程中执行,则可以将该数据可见范围限制在这个线程中,这样不用同步,其他线程也不会争夺该数据

看一个实例

public class SequenceNumber {
    //通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
    private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
        public Integer initialValue() {
            return 0;
        }
    };
    public int getNextNum() //获得下一个序列值
    {
        seqNum.set(seqNum.get()+1);
        return seqNum.get();
    }
    public static void main(String[] args) {
        SequenceNumber sn = new SequenceNumber();
        TestClient t1 = new TestClient(sn);
        TestClient t2 = new TestClient(sn);
        TestClient t3 = new TestClient(sn);
        t1.start();
        t2.start();
        t3.start();
    }

}
class TestClient extends Thread{
    private SequenceNumber sn;
    public TestClient(SequenceNumber sn)
    {
        this.sn = sn;
    }
    public void run()
    {
        for(int i=0; i<=2; i++)
            System.out.println("Thread["+Thread.currentThread().getName()+"]sn["+sn.getNextNum()+"]");
    }
}

输出结果

Thread[Thread-0]sn[1]
Thread[Thread-0]sn[2]
Thread[Thread-1]sn[1]
Thread[Thread-1]sn[2]
Thread[Thread-1]sn[3]
Thread[Thread-0]sn[3]
Thread[Thread-2]sn[1]
Thread[Thread-2]sn[2]
Thread[Thread-2]sn[3]

这个输出结果不唯一,但是每一个线程产生的序列号都是递增的,他们共享的是同一个SequenceNumber,但是他们产生的序列值却不会互相干扰,这就实现了同步(不用使用synchronized),而这个序列值就是前面的线程本地存储,该值只在该线程中可见

锁优化

  • 自旋锁

    我在操作系统的一篇文章写过用户级线程和内核级线程的切换,用户级线程和内核级线程,看了这个只有你就会知道,切换线程其实对操作系统的负担是挺重的,因此我们希望当一个线程请求锁时,请求不到也在那里等一会(忙循环),一般是循环10次等待,看看等一会是否能得到锁

  • 自适应锁

    自适应自旋,前面的自选是固定时间的,而我们希望该线程能够根据上一个在该锁等待的线程等待的时间以及占有该锁的线程的状态来自适应的调整等待时间,如果上一个线程通过自选等待得到了锁,那么我们就认为这次等待可能还会成功,因此增加自旋时间

  • 锁消除

    定义:JVM即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除
    判断依据:如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当作栈上的数据对待,认为它们是线程私有的,同步加锁自然无需进行(ps:每一个线程都有一个属于自己的栈)

  • 锁粗化

    我们写代码的时候通常会将同步块的范围限制的尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得同步操作的数量尽可能变小

    但是,还有另一种情况,如果你有一连串的代码都是对同一个对象反复加锁,那么就算没有线程争夺,依然会降低效率,因此我们会将这些区域用一个锁来处理,这就是锁粗化

  • 偏向锁

    目的:消除数据无竞争情况下的同步原语,进一步提高程序运行的性能,偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS都不做
    偏向:意思是这个锁会偏向第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步

参考

  }
}

输出结果

Thread[Thread-0]sn[1]
Thread[Thread-0]sn[2]
Thread[Thread-1]sn[1]
Thread[Thread-1]sn[2]
Thread[Thread-1]sn[3]
Thread[Thread-0]sn[3]
Thread[Thread-2]sn[1]
Thread[Thread-2]sn[2]
Thread[Thread-2]sn[3]

这个输出结果不唯一,但是每一个线程产生的序列号都是递增的,他们共享的是同一个SequenceNumber,但是他们产生的序列值却不会互相干扰,这就实现了同步(不用使用synchronized),而这个序列值就是前面的线程本地存储,该值只在该线程中可见

锁优化

  • 自旋锁

    我在操作系统的一篇文章写过用户级线程和内核级线程的切换,用户级线程和内核级线程,看了这个只有你就会知道,切换线程其实对操作系统的负担是挺重的,因此我们希望当一个线程请求锁时,请求不到也在那里等一会(忙循环),一般是循环10次等待,看看等一会是否能得到锁

  • 自适应锁

    自适应自旋,前面的自选是固定时间的,而我们希望该线程能够根据上一个在该锁等待的线程等待的时间以及占有该锁的线程的状态来自适应的调整等待时间,如果上一个线程通过自选等待得到了锁,那么我们就认为这次等待可能还会成功,因此增加自旋时间

  • 锁消除

    定义:JVM即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除
    判断依据:如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当作栈上的数据对待,认为它们是线程私有的,同步加锁自然无需进行(ps:每一个线程都有一个属于自己的栈)

  • 锁粗化

    我们写代码的时候通常会将同步块的范围限制的尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得同步操作的数量尽可能变小

    但是,还有另一种情况,如果你有一连串的代码都是对同一个对象反复加锁,那么就算没有线程争夺,依然会降低效率,因此我们会将这些区域用一个锁来处理,这就是锁粗化

  • 偏向锁

    目的:消除数据无竞争情况下的同步原语,进一步提高程序运行的性能,偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS都不做
    偏向:意思是这个锁会偏向第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步

参考

清华大学Java教程p105-p124

猜你喜欢

转载自blog.csdn.net/jump_into_zehe/article/details/106854446
今日推荐