java线程启动、安全问题(原子性、可见性)

启动线程的2种方法

psvm(){
	/*方法1*/
	Thread t1 = new Thread(){
	            int num;
	            @Override
	            public void run() {
	            //process
	            }
	};
	t1.start();  //只能启动一次
	t1.start();	 //报错!!!
	Thread t2 = new Thread(){
	            int num;
	            @Override
	            public void run() {
	            //process  num++;
	            }
	};
	t2.start();  
	/*方法2*/ 
	Runnable r1 = new Runnable() {
				int num;
	            @Override
	            public void run() {
	                //process num++;;
	            }
	};
	        new Thread(r1).start(); //可以new多个线程启动多次
	        new Thread(r1).start();	//new出来一个新线程,可以再启动
}

法1 线程t1只能启动1次,要想再启动就要创造新的线程t2,且每次启动新的线程对num的操作都是局部的,
法2 中把Runable对象封装到多个线程中启动, 则对num的操作是全局的

  • Thread
    • 类 只能单继承,不够灵活
    • Thread 每次只能启动一次 ,对象就只能用在一个线程中
  • Runnable
    • 接口 可以多实现,比较灵活
    • 不依赖启动方法,一个对象被封装到多个Thread对象中启动

匿名内部类

前文是匿名内部类写法
改写成 正常写个类,再new个对象 :

class ThreadAddNum extends Thread{
    int num;
    @Override
    public void run(){
        for(int i = 0; i < 10000; i++){
            num++;
        }
    }
}

class RunnableAddNum implements Runnable {
    int num;
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            num++;
        }
    }
}

public class thread_test {

    public static void main(String[] args) {
        ThreadAddNum t1 = new ThreadAddNum(), t2=new ThreadAddNum();
        t1.start();
        t2.start();

        RunnableAddNum r1 = new RunnableAddNum();
        new Thread(r1).start();
        new Thread(r2).start();
    }
}

线程安全问题

原子性 与 synchronized

稍微改动下前文的代码

class ThreadAddNum extends Thread{
    int num;
    @Override
    public void run(){
        for(int i = 0; i < 10000; i++){
            num++;
        }
    }
}

class RunnableAddNum implements Runnable {
    int num;
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            num++;
        }
    }
}

public class thread_test {

    public static void main(String[] args) {
        ThreadAddNum t1 = new ThreadAddNum(), t2=new ThreadAddNum();
        t1.start();
        t2.start();
		
		RunnableAddNum r1 = new RunnableAddNum();
        Thread rt1 = new Thread(r1) , rt2=new Thread(r1);
        rt1.start();
        rt2.start();
		//等待rt1,rt2两个线程结束
        try {
            rt1.join();
            rt2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(r1.num);
    }
}

功能是一样的 ,只是把用runnable启动的线程改变后的num输出
发现每次num结果不一样.并且一般都不是20000.
按我们理解来说,答案应该是2w,问题出在哪里???

问题在于 2个线程并行的对num操作,且改变num的过程是写读num,再++,再写回.
如此会导致可能线程1,2同时读了num++再同时写回,就相当于只加了1次,或者线程1读了num,还没写回,线程2又去读num,然后线程1写回,2写回,线程2写回的结果覆盖了线程1,又相当于只加了1次…

引出了 线程不安全:

  • 线程不安全:
    可以有多个线程同时操作一个资源时没有受到管制,会线程不安全
  • 解决方法: 对于资源的操作原子性保证
    • 原子性: 一口气完成,不会中断…资源的每次操作不会影响到其他线程读取的结果
synchronized

协调,同步 之意
synchronized用法
synchronized原理

这里改写一下RunnaleAddNum类就行,下面列举了3个改写方法:

//法1, synchronized修饰方法
class RunnableAddNum implements Runnable {
    int num;
    @Override
    synchronized  public void run() {
        for (int i = 0; i < 10000; i++) {
                num++;
        }
    }
}
//法2,synchronized(Object)修饰代码块,object用 this
class RunnableAddNum implements Runnable {
    int num;
    Object l = new Object();
    @Override
    synchronized  public void run() {
        for (int i = 0; i < 10000; i++) {
            synchronized (this){
                num++;
            }
        }
    }
}
//法3,synchronized(Object)修饰代码块,object用l
class RunnableAddNum implements Runnable {
    int num;
    Object l = new Object();
    @Override
    synchronized  public void run() {
        for (int i = 0; i < 10000; i++) {
            synchronized (l){
                num++;
            }
        }
    }
}

可见性 与 volatile

class RunnbaleVt implements Runnable{
    //volatile boolean flag = true;
    boolean flag = true;
    @Override
    public void run(){
        System.out.println ("启动线程.....");
        while(flag){
//            System.out.print("");
        }
        System.out.println ("执行任务完成.....");

    }
}

public class thread_test {
    	//主线程
    public static void main(String[] args) {
        RunnbaleVt rtv = new RunnbaleVt();
        //启动线程1
        new Thread(rtv).start();
		//启动线程2
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                rtv.flag=false;
                System.out.println(rtv.flag);
                System.out.println("rtv.flag=false;");
            }
        }.start();
    }
}

分析一下这段代码, 线程1开启了RunnbaleVt,会执行循环,flag=false时跳出循环,打印"执行任务完成"
然后开启线程2更改了false值,但执行发现线程1始终不会跳出循环.
解释
在这里插入图片描述

  • 因为对象都在堆内存(主存)中,堆内存是被各线程共享,
  • 而线程1,2分别有私有的栈空间,
  • 由于现在的cpu多核,不同的线程在不同的cpu上运行,有不同的cache系统,由于缓存机制,线程第一次从堆内存中读了flag后,就会缓存在各自的cache中,之后的读写都是与自己的cache进行.
    故线程2更新的flag不会被线程1读.
  • 这就是可见性问题. 可以用volatile解决. (见图)

volatile

一篇关于volatile很好的讲解

总结出几点关键的:

  • volatile 关键字是用来修饰变量
  • 通过 volatile 修饰的变量,所有关于该变量的读操作,都会直接从主内存中读取,而不是 CPU 自己的缓存。而所有该变量的写操都会写到主内存
  • 主要解决2个:
    1. 多线程间可见性的问题,
    2. CPU 指令重排序的问题
  • Java volatile Happens-Before 规则保证了 指令不会乱序发射,且使得 volatile能有更多的效果:
    1. 当 Thread A 修改了某个被 volatile 变量 V,另一个 Thread B 立马去读该变量 V。一旦 Thread B 读取了变量 V 后,不仅仅是变量 V 对 Thread B 可见, 所有在 Thread A 修改变量 V 之前 Thread A 可见的变量,都将对 Thread B 可见。
    2. 当 Thread A 读取一个 volatile 变量 V 时,所有对于 Thread A 可见的其他变量也都会从主内存中被读取。
  • 不能保证原子性
  • 效率方面:
    如果大家了解 CPU 的多级缓存机制,(不了解应该也能猜到),从主内存读取数据的效率一定比从 CPU 缓存中读取的效率低很多。包括指令重排序的目的也是为了提高计算效率,当重排序机制被限制时,计算效率也会相应收到影响。因此,我们应该只在需要保证变量可见性和有序性时,才使用 volatile 关键字。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值