Java并发编程基础(入门篇)

@

java中的多线程(入门)

1.线程

1.1多线程的执行原理

先以一个Java中多线程的Demo为例展开解释,请看代码:

//自定义的多线程类
public class MyThread extends Thread{
    public MyThread(String name){
        super(name);
    }
    //重写run方法
    @Override
    public void run(){
        for(int i=0;i<20;i++){
            System.out.println(getName()+"_"+i);
        }
    }
}
//测试类
public class ThreadTest {
    public static void main(String[] args) {
        //多线程的执行原理及其步骤:
        //1.Jvm执行main方法,找OS开辟一条通向cpu的路径,这个
        //路径就叫mian线程(主线程),cpu通过这个路径执行main中的方法
        //新开辟一条通向cpu的新路径,这条新路径就是MyThread线程
        MyThread mt=new MyThread("bob");
        //使用MyThread线程执行run方法
        mt.start();
        for(int i=0;i<20;i++){
            System.out.println("mian_"+i);
        }
    }
}

上述代码执行流程:

  • 当jvm执行main方法时候,jvm向OS申请开辟一条通往cpu的可执行路径,这条路径叫main线程。(cpu通过这条路径来执行main的所有方法)
  • 继续执行,MyThread mt=new MyThread("bob"),jvm向os申请开辟一条新的通往cpu的路径,该路径为MyThread的线程。
  • mt.start();cpu执行MyThread线程。
  • 由于cpu既可以通向main线程,又可以通向MyThread线程,我们程序无法控制cpu的选择,其本身具备线程的调度策略,故两个线程会出现交叉执行的随机现象。
  • main线程和MyThread线程处于并发的状态,两者轮流使用cpu。
    图解多线程执行内存情况
    在这里插入图片描述
    注意: mt.start()方法做了两件事情:为mt线程开辟了新的栈空间;将该线程的run方法压栈执行;

1.2多线程的创建方法

方法1: 继承Thread类,在子类中重写run()方法,start()方法开启新的线程;

public class MyThread extends Thread{

    public MyThread(String name){
        super(name);
    }

    @Override
    public void run(){
        for(int i=0;i<20;i++){
            System.out.println(getName()+"_"+i);
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
         MyThread mt=new MyThread("bob");
         mt.start();
    }
}

方法2: 实现Runnnable接口,将该子类作为参数传递给Thread的构造方法;使用Thread对象的start()方法启动新的线程;

public class MyRunnable implements Runnable{
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"_"+i);
        }
    }
    
}
public class ThreadTest {
    public static void main(String[] args) {
        Thread th=new Thread(new MyRunnable());
        th.start();
        System.out.println("主线程");
    }
}

1.3Thread和Runnable的区别

实现Runnable的好处:

  • Runnable避免java中的单继承的局限性
  • 增强了程序的扩展性,降低了程序的耦合性;把设置线程任务和开启线程任务实现了分离
  • 多个线程可以共享同一个线程任务
  • 线程池只能放入实现了Runnable或者Collable接口的类实例,不能放置继承Thread类的子类对象;

1.4匿名内部类方式实现线程的创建

public class ThreadTest {
    public static void main(String[] args) {
        //匿名内部类
        //规则:new 父类/接口(){
            //重写父类中的run方法;
        //}
        new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName());
                }
            }
        }).start();
        System.out.println("main");
    }
}

2.线程的安全

多线程访问了共享数据会导致线程问题
卖票案例

public class Ticket implements Runnable {
    private int tickets=100;
    @Override
    public void run(){
        while(true){
            if(tickets>0){
                try{
                    Thread.sleep(100);
                }catch(Exception e){
                    e.printStackTrace();
                }
                String name=Thread.currentThread().getName()+"正在卖票";
            System.out.println(name+"正在卖票:"+tickets--);
            }
        }
    }   
}
public class SaleTickets {
    public static void main(String[] args) {
        Ticket ticket=new Ticket();
        Thread t1=new Thread(ticket,"窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

产生安全问题的原因

多个线程访问了同一个变量。

使用java的线程同步机制来解决线程安全问题

  • synchronized同步代码块
  • synchronized同步方法
  • lock锁机制(可以手动加锁和释放锁)
//同步块
public class Ticket implements Runnable {
    private int tickets=100;
    Object obj=new Object();
    @Override
    public void run(){
        while(true){
            if(tickets>0){
                synchronized(obj){
                    try{
                        Thread.sleep(100);
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                    String name = Thread.currentThread().getName() + "正在卖票";
                    System.out.println(name + "正在卖票:" + tickets--);
                }
            }   
        }
    }   
}
//同步方法块
public class Ticket implements Runnable {
    private int tickets=100;
    Object obj=new Object();
    @Override
    public void run(){
       saleTicket();
    }
    public synchronized void saleTicket(){
        while (true) {
            if (tickets > 0) {
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                String name = Thread.currentThread().getName() + "正在卖票";
                System.out.println(name + "正在卖票:" + tickets--);
            }
        }
    }
}
//使用lock锁机制
public class Ticket implements Runnable {
    private int tickets=100;
    //创建锁对象
    Lock lock=new ReentrantLock();
    @Override
    public void run(){
        while (true) {
            lock.lock();//加锁
            if (tickets > 0) {
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                String name = Thread.currentThread().getName() + "正在卖票";
                System.out.println(name + "正在卖票:" + tickets--);
            }
            lock.unlock();//释放锁
        }
    }
}

同步原理

t0、t1、t2三个线程同时去抢夺cpu资源,假如t0获取到了cpu执行权限,t0遇到synchronized(object),获取对象锁,进入到同步代码块,当同步代码块执行完成之后,才归该对象锁。

3.线程的状态

  • new(可运行,但没有运行.start())
  • Runnable(正在运行的线程)
  • blocked(没有获取到锁对象,有cpu执行权力)
  • terminated(终止状态,run方法完成,异常终止)
  • timed_waiting(计时等待,时间到了,自己恢复)
  • waiting(永久等待,等待锁对象调用notify()唤醒)

生产者消费者案例

  • 同步锁对象是唯一,并且是多个线程所共享的
  • 使用锁对象的wait()和notify()来使得当前线程休眠和唤醒,两个方法都必须处于synchronized的代码块或方法中;
public class WaitAndNotify {
    public static void main(String[] args) {
        //保证生产者和消费者使用同一个对象锁
        Object obj=new Object();
        //消费者
        new Thread(new Runnable(){
            @Override
            public void run() {
                while(true){
                    synchronized (obj) {
                        System.out.println("我要吃包子");
                        try {
                            obj.wait();// 当前线程进入永久等待,并且规划对象锁,直到收到唤醒通知;
                            System.out.println("开始吃包子");
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();
        //生产者
        new Thread(new Runnable(){
            @Override
            public void run() {
                while(true){
                    // 唤醒消费者线程
                    synchronized (obj) {
                        try {
                        // 生产包子,1秒
                        Thread.sleep(1000);
                        } catch (Exception e) {
                        e.printStackTrace();
                    }
                        obj.notify();//发出唤醒由该对象锁进入等待的线程,代码出了synchronized代码块后自动归还对象锁;
                    }
                }  
            }
        }).start();
    }
    
}

使用了object.wait()之后,当前线程处于永久等待,并且归还该对象锁,直到收到object.notify()的唤醒通知。


并发编程

并发编程的硬件基础

现代CPU架构中,一个CPU通常可以包含多个物理内核,而一个内核又通过虚拟技术可以实现双线程,因此一个两核CPU可以同时运行4个线程。

什么是并发?

在指定的一段时间内,可同时运行多个线程,并发编程的目的在于确定每个线程任务分配的cpu及其相应的开始和结束时间以使得cpu的利用率最高,同时必须保证线程的安全!

并发编程的三大特性

原子性
即定义的一系列操作要么全部执行成功,要么一个都不执行。
可见性
多个线程共享同一个变量的时候,当一个线程修改了该变量的时候,其他线程必须能够立刻获取到修改后的值。
有序性
程序执行的顺序按照代码的先后顺序执行;java编译器为了提高程序效率,会对代码的指令进行重排,在单线程程序中,会保证程序最后的执行结果和重排序之前一致,但是在多线程程序中,有可能会影响并发的结果。因此,在并发编程中,必须保证有序性


java如何保证并发编程的三大特性

Java的内存模型

Java内存模型规定,所有的变量均存储在主存(物理内存),每一个线程都拥有自己的工作内存,也叫缓存(主存中的副本,线程不直接修改主存中的变量,每个线程不能访问其他线程的工作内存)cpu中还有高速缓存行(l1,l2,l3寄存器),线程的工作内存数据先被读取到cpu缓存器中,因此数据存在三个级别的。举个例子:

 i=250;

执行该行代码的时候,先从主存中取i的值到当前线程的工作内存中,然后将250赋值给工作内存中的i变量,最后,将工作内存中的i变量写入到主存中去。


原子性
java中对单个的读或者单个的写操作,是具有原子性的,即要么成功,要么失败。但是对任何单一的操作的组合而成的复杂操作是无法保证原子性的。举个例子:

i++;

i++自增操作不具备原子性的,为什么?i++操作其实是由三个操作构成:1)读取i的值。2)i的值增1. 3)将新值赋值给i变量;java语言只能保证单一的操作具有原子性。而要保证诸如i++等的原子性,可以使用synchronizedlock锁机来同步这些代码块,可以保证并发编程的原子性。

可见性
那java中如何保证变量的可见性呢?volatile关键字。当被volatile修饰的变量(当前线程的共内存中)被修改时,能被立即刷新到主存中去。因此,可以保证在其他线程还没有从主存中读去新值的情况下,可以得到最新的值。 ,同时 synchronized 和 lock能保证在同一个时刻只有一个线程能获取到对象锁(对象监视器)然后执行同步代码块,并且在释放对象锁之前将修改的值刷新到主存中去,从而在一定程度上保证了变量的可见性。

有序性
Java内存模型中,允许编译器和处理器对指令进行重排,指令重排不会影响共到单线程,但是会影响到多线程并发执行的正确性。java中的"happens-before"原则能保证一定的有序性。"happen-before"有8大原则:

  • 程序次序规则。在一个程序内,书写在前面的操作先于后面的操作
  • 锁定规则。同一锁对象的Lock操作先于Unlock操作
  • volatile变量规则。线程1对该变量的写操作如果先于线程2对该变量的读操作,那么线程2的读操作才能读取到线程1写入的值。
  • 传递规则。如果A先于B,B先于C ,那么A一定先于C
  • 线程启动规则。Thread对象的start()方法先于此线程的任意动作
  • 线程中断规则。对线程的interupt()操作一定先于该线程中断事件发生和检测。
  • 线程终止规则。线程的终止检测是该线程中最后的操作,因此可以使用Thread.join()来终止线程和Thread.isAlive()来检测线程是否终止
  • 对象终结规则。一个对象的初始化操作先于对象的finalize()操作的开始。

volatile关键字的详细解释

一个共享变量如果被volatile所修饰,那么该变量就具备以下两种含义:

  • 该变量对于所有线程具有可见性
  • 该变量及其相关变量(有依赖关系)不允许指令重排,从而保证了有序性。
    举个例子,以下代码线程1先执行。
//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
//线程2
stop = true;

分析: 线程1中执行以下操作,先将主存中的stop变量读取到线程1的工作内存中,线程1进入死循环,一直执行doSomething()操作。而当线程2开始执行的时候,情况变了,线程2先将主存中的stop变量读取到工作内存中,此时有可能存在两种情况:1)线程2修改了工作内存中的stop变量,但是没有写入到主存中去就被终止了。 2)线程2修改了工作内存中的stop变量,并写入到主存中去。当情况1)发生时,那么线程1进入死循环。当情况2)发生时,线程2可以退出死循环。那么如何才能保证上述代码只正确执行呢??使用volatile修饰stop保证这个变量的可见性,即当这个变量在某个线程的工作内存中修改是就立即被刷新到主存中,其他线程工作内存中的缓存状态失效,并重新从主存中读取新值。


volatile能保证原子性吗
先上个案例,

public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        while(Thread.activeCount()>1) {
            //保证前面的线程都执行完
            Thread.yield();
        } 
        System.out.println(test.inc);
    }
}

分析: 代码创建了10个线程,每个线程使得inc自增1000,同时使用volatile修饰了inc变量,那inc最终的结果是10*1000吗??答:不是。为什么呢?因为虽然inc具有可见性,但是inc++操作不具备原子性,我们可以设想到这样的一个场景,某个状态下,inc=10,线程1从主存中读取了inc的值到工作内存中,inc还未进行自增的时候,线程1的cpu执行权限到了,转而去执行线程2,此时,线程2从主存中将inc读取到工作内存中,再将Inc读取到cpu的高速缓存寄存器中,进行自增操作inc+1=11,并写回主存,线程1已经读取了inc到工作内存中并且读取到cpu的l1、l2中去了,即使缓存中的数据更新过来,cpu用的仍然是更新之前的数据,因此也不会重新读取缓存中的值随后刷新到主存中,因此两个线程inc增加了1。

改进
使用synchronized或则lock机制来保证inc操作具有原子性。就可以让线程1准确地保证可见性,从使得并发能正确进行

public class Test {
    public volatile int inc = 0;
    Lock lock = new ReentrantLock();
     //使用syunchronized保证原子性
    public synchronized void increase() {
        inc++;
    }
    //使用lock同步代码块,保证原子性
    public void increase2(){
        try{
            lock.lock();
            inc++;
            lock.unlock();
        }catch(Exception e){
            ..
        }
    }
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        while(Thread.activeCount()>1) {
            //保证前面的线程都执行完
            Thread.yield();
        } 
        System.out.println(test.inc);
    }
}

结论: volatile 能保证共享变量的可见性,但是无法保证对该变量的操作的原子性,保证原子性可以通过synchronized或者lock来保证。

volatile的作用

  • 缓存一致性协议。cpu的硬件协议,每个cpu通过嗅探总线上传输的数据来检测自己的缓存中的数据是否失效,当cpu'发现自己的缓存行(l1,l2,l3等寄存器,存放的是缓存的地址)中对应的内存中地址被修改的时候,就会将当前cpu的缓存设置为无效状态,当cpu对这个数据进行操作的时候,就会重新从主存中读取数据到缓存中。通过这种方式能保证可见性
  • 禁止指令重排。假如一个变量被volatile修饰,那么与这个变量有依赖关系的前面所有变量的操作顺序只能先于该变量,同理其后的所有变量的操作在它之后执行,不允许被重排

线程内的共享变量(ThreadLocal)

ThreadLocal对象用于某个线程内的共享变量,ThreadLocal对象提供两个方法:set()和get()方法,两个方法实现底层为一个Map,Map的键为当前线程的ThreadLocal对象。因此不同的线程使用get()能拿到属于自己线程内的ThreadLocal对象,也就能保证拿到其中的值;同时,在同一个线程中能保证一个ThreadLocal对象只存储一个值,实现了线程内的共享。看个例子吧:

//Runnable的子类
public class MyThreadLocal implements Runnable {
    //private Student student=new Student();
    ThreadLocal<Student> tl=new ThreadLocal<Student>();
    @Override
    public void run(){
        String currentThread=Thread.currentThread().getName();
        System.out.println(currentThread+"正在运行");
        System.out.println("开始设置对象的属性");
        Random random=new Random();
        Student student = getStudent();
        student.setAge(random.nextInt(100));
        System.out.println(currentThread+"线程中,第一次的年龄为:"+student.getAge()+",哈希值为:"+student.hashCode());
        try {
            Thread.sleep(1000);//让其他线程运行;
        } catch (Exception e) {
            e.printStackTrace();
        }
        student.setAge(random.nextInt(100));
        System.out.println(currentThread + "线程中,第二次的年龄为:" + student.getAge() + ",哈希值为:" + student.hashCode());
    }
    public Student getStudent(){
        Student s=tl.get();
        if(null==s){
            s=new Student();
            tl.set(s);
        }
        return s;
    }
}
//测试类
public class testThreadLocal {
   public static void main(String[] args) {
        MyThreadLocal threadLocal=new MyThreadLocal();
        new Thread(threadLocal,"线程1").start();
        new Thread(threadLocal,"线程2").start();
   }
}

分析:在创建要线程内共享的变量之前,先从ThreadLocal中get()一份,如果有就不创建,没有才创建,并且使用set()方法将刚刚创建的对象存到ThreadLocalMap中去。


多个线程之间的数据共享

java传统的对线程间的数据共享通常有两种形式:

  • 线程行为一致。多个线程使用同一个Runnable对象,即执行相同的代码段,共享数据在Runnable对象中。
  • 线程行为不一致。多个线程的行为不一致,需要使用不同的Runnable对象,执行不同的代码段。
    线程行为一致的代码可以参考上述的卖票系统,现在举例说明线程不一致的数据共享情况:
    方法1 将共享数据封装,然后作为参数传递至Runnable对象
public class testShareData {    
    public static void main(String[] args) {
         ShareData shareData=new ShareData(10);
         for(int i=0;i<4;i++){
             if(i%2==0){
                 new Thread(new IncRunnable(shareData), "Thread_" + i).start();
             }else{
                 new Thread(new decRunnable(shareData), "Thread_" + i).start();
             }
             
         }
    }   
}

// 共享数据的自增行为的Runnable类
class IncRunnable implements Runnable {
    private ShareData shareData;

    public IncRunnable(ShareData shareData) {
        this.shareData = shareData;
    }

    private void incShareData() {
        shareData.inc();
    }

    @Override
    public void run() {
        incShareData();
    }
}

// 共享数据的自减行为的Runnable类
class decRunnable implements Runnable {

    private ShareData shareData;

    public decRunnable(ShareData shareData) {
        this.shareData = shareData;
    }

    private void decShareData() {
        shareData.dec();
    }

    @Override
    public void run() {
        decShareData();
    }
}
public class ShareData {
    // 共享数据封装类
    private int num;
    public ShareData(int num) {
        this.num = num;
    }
    // 数据自增
    public synchronized void inc() {
        num++;
        System.out.println(Thread.currentThread().getName()+"num值为:"+num);
    }
    // 数据自减
    public synchronized void dec() {
        num--;
        System.out.println(Thread.currentThread().getName() + "num值为:" + num);
    }
}

方法2 使用匿名内部类的方式创建Runnable对象,可以减少参数传递的步骤。

public class testShareData {    
    public static void main(String[] args) {
         //确保shareData这个引用只会指向一个new ShareData()对象
         final ShareData shareData=new ShareData(10);
         for(int i=0;i<4;i++){
             if(i%2==0){
                 new Thread(new IncRunnable(){
                     @Override
                     public void run(){
                        shareData.inc();
                     }
                 }, "Thread_" + i).start();
             }else{
                 new Thread(new decRunnable(){
                    @Override
                     public void run(){
                        shareData.dec();
                     }
                 }, "Thread_" + i).start();
             }
             
         }
    }   
}
public class ShareData {
    // 共享数据封装类
    private int num;
    public ShareData(int num) {
        this.num = num;
    }
    // 数据自增
    public synchronized void inc() {
        num++;
        System.out.println(Thread.currentThread().getName()+"num值为:"+num);
    }
    // 数据自减
    public synchronized void dec() {
        num--;
        System.out.println(Thread.currentThread().getName() + "num值为:" + num);
    }
}

总结面向对像的特性之一:封装;

posted on 2020-06-18 11:37  小毅哥哥Bob  阅读(272)  评论(0编辑  收藏  举报

导航