线程安全;解决线程问题

------------------------------------------------- 线程-----------------------------------------------------------

  1. 进程与线程的概念:
  2. 并发与并行;
  3. 多线程的创建(两种方式);
  4. 高并发问题;
  5. 解决不可见性、无序性、非原子性的方法;

一、进程和线程的概念

进程

进入到内存中的程序叫做进程

线程

线程是进程的一个执行单元,负责当前进程中程序的执行,一个进程中至少含有一个线程;(进程中只有一个线程,这种进程叫做单线程程序;进程中含有两个及以上的线程个数,这种进程叫做多线程程序)

引入到我们的JAVA代码中

一般情况下,我们的程序都会在main方法中自上而下的执行,执行main方法是一个线程,叫做主线程;一般情况下,我们不创建线程的条件下,我们的代码中有主线程、抛出异常的线程、清理垃圾的线程等。

线程的六种状态

New(新建)、Runnable(可运行/就绪)、Blocked(阻塞)、Waiting(等待)、Time_Waiting(超时等待)、Terminated(终止)

start()和run()的区别

start()会开启线程;
run()只会调用方法;

二、并发与并行

我们在玩电脑的时候,电脑会运行许多的软件,每个软件又内含各自不同的功能,他们的运行需要CPU的支持。而我们的CPU不能一次性执行全部的线程,只能一次性执行几个线程,或者以极高的速度在多个线程直接来回跳转;所以就有了并发与并行:
并行:CPU可以一次性执行多个线程,在执行这个线程的同时,也可以执行另外的一个线程,这种同时执行的方式叫做并行。
并发:CPU由于不能一次性执行完所有的线程,这个时候,他就会以极高的速度在线程之间来回转换,在一段时间内,他通过极高的速度在线程中转换,执行每个线程,这种情况叫做并发;

三、多线程的创建

我们在程序运行的时候,当然不能只有一个人使用,而是供很多人使用的,这时我们就需要创建多个线程,来供每个人使用。

关于多线程的创建,我们有以下两种方式:

方法一:

继承Thread类(通过重写里面的run方法来创建线程);
我们需要创建两个类,一个测试类,一个Thread的子类,通过两个线程来打印 0 到100,代码如下:

Demo01Thread测试类:

public class Demo01Thread {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        thread1.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("main线程打印输出i值" + i);
        }
    }
}

MyThread类:

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("创建的线程打印输出i值" + i);
        }
    }
}

截图如下:
在这里插入图片描述

上图我们会发现,两个线程在抢占CPU的执行权,一会主线程执行,一会我们自己创建的线程执行;
关于以继承的方式来创建线程,由于在JAVA中只能单继承,你创建线程的过程时,继承了Thread就不能在继承其他的类,就会限制你创建的代码继承其他的类,这个时候,我们就可以使用接口来创建线程;方法如下:

方法二、实现Runnable接口;
通过继承的方式创建线程,有很大的局限性,JAVA语言只支持单继承,但是它支持多实现,所以我们可以通过Runnable接口来创建线程,我们也需要一个测试类,一个实现类,代码如下:

Demo02Runnable测试类代码如下:

public class Demo02Thread {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread1 = new Thread(myRunnable);
        thread1.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程打印i:" + i);
        }
    }
}

MyRunnable代码如下:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("创建的线程打印i:" + i);
        }
    }
}

截图如下:
在这里插入图片描述

方式三、我们还可以通过匿名内部类来创建线程;
格式: new 爹(){
重写父类|接口中的方法!
};
代码如下:

public class Demo03Thread {
    public static void main(String[] args) {
        //内部类创建线程:内部列通过父类创建线程
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("(继承父类Thread)内部类线程打印i: " + i);
                }
            }
        }.start();
        //内部类创建线程:内部类通过接口创建线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("(实现接口Runnable)内部类线程打印i: " + i);
                }
            }
        }).start();

        for (int i = 0; i < 100; i++) {
            System.out.println("main线程开始打印i : " + i);
        }
    }
}



截图如下:
在这里插入图片描述

线程使用注意事项:

   创建的线程只能使用一次,不能多次启动,不然会报错(IllegalThreadStateException)

四、高并发问题

什么是高并发,在某个时间点上,有多个线程同时访问某一个资源;举个例子,双十一秒杀活动。当多个线程无序的访问同一个资源(例如同一个变量,同一个数据库,同一个文件等等),而且访问的同一资源不具有“原子性”,这时对这一资源的方法就会产生安全性问题-------导致此资源最终结果是错误的。

高并发产生的安全性问题主要表现在:

 1、不可见性:指的是当多个线程访问同一个变量时,一个线程改变了这个变量的值,其他线程不能立刻看见,使用的还是没改变之前的值。
 2、无序性:程序执行的顺序不按照代码的先后顺序执行。
 3、非原子性;

代码举例;

1、不可见性:
Demo04Thread代码:

public class Demo04Thread {
    public static void main(String[] args) {
        NoSee noSee = new NoSee();
        Thread thread = new Thread(noSee);
        thread.start();
        while(true){
            if(NoSee.a == 1) {
                System.out.println("主线程接收到线程更改后的a的值;");
                break;
            }
        }
    }
}

NoSee代码:

public class NoSee implements Runnable{
    public static int a = 0;
    @Override
    public void run() {
        //睡眠2秒钟,等待main执行while方法
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        //睡眠结束,开始更改a的值
        System.out.println("创建的线程执行了,更改a的值");
            a = 1;
    }
}

运行结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/cb232dcce15d4524985f980f5b6c6aae.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl81MDkyNzk5Mw==,size_16,color_FFFFFF,t_70#pic_center在这里插入图片描述

你会发现,明明你创建的线程已经更改了变量a的值为1,但是主线程仍然还在执行死循环,这就是高并发带来的问题之不可见性,如何解决,在三个特性之后会解决。

2、无序性:

我们在写java代码的时候,写的代码如下:

int a = 10;
int b = 20;
int c = a + b;

但是在生成的class文件中,代码的顺序可能就变了:

int b = 20;
int a = 10;
int c = a + b;

虽然对于现在的结果没有影响,但是这只是简单的例子,我们在写代码的时候当然不希望他的顺序改变,这不但会影响我们的代码,甚至会影响到最终结果。

3、原子性:

测试代码:Demo01ThreadRunnable

public class Deo01ThreadRunnable {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        System.out.println(Thread.currentThread().getName() + "执行了!");
        for (int i = 0; i < 10000; i++) {
            MyRunnable.money++;
        }
        System.out.println(Thread.currentThread().getName() + "结束任务了!");
        System.out.println("money的最终值为:" + MyRunnable.money);
    }
}

线程代码:

public class MyRunnable implements Runnable{
    public static int money = 0;
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "执行了!");
        for (int i = 0; i < 10000; i++) {
            money++;
        }
        System.out.println(Thread.currentThread().getName() + "结束任务了!");
    }
}

截图:
在这里插入图片描述

上图你可以发现,两个线程分别调用money变量,分别给其值增加10000,但是最终结果却不是20000,这说明线程在执行某一部的时候,可能被暂停了(失去了CUP的执行权),执行另外一个线程,就会导致最终结果出现错误。

五、解决不可见性、无序性、非原子性的方法

1、Volatile关键字(可以解决不可见性、无序性)

格式:在成员变量中添加Volatile关键字:

 public static volatile int a = 0;

示例代码:
1、解决不可见性:

Demo04Thread代码:

public class Demo04Thread {
    public static void main(String[] args) {
        NoSee noSee = new NoSee();
        Thread thread = new Thread(noSee);
        thread.start();
        while(true){
            if(NoSee.a == 1) {
                System.out.println("主线程接收到线程更改后的a的值;");
                break;
            }
        }
    }
}

NoSee代码:

public class NoSee implements Runnable{
    public static volatile int a = 0;
    @Override
    public void run() {
        //睡眠2秒钟,等待main执行while方法
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        //睡眠结束,开始更改a的值
        System.out.println("创建的线程执行了,更改a的值");
            a = 1;
    }
}

运行截图如下:
在这里插入图片描述

你会发现while循环开始执行了,它取到了线程更改后a的值。

原理:在线程运行的时候,两个线程都会得到一个变量a的副本,线程在执行的时候,拿到的副本没有失效,就会一直使用,而volatile关键字的作用,是为了让其刚开始拿到的副本失效,这样就会迫使线程重新获取副本,得到新的值。

volatile也可以解决有序性;

注意:volatile只能写在方法外,类中。

volition不能解决非原子性,这个时候我们就需要别的办法了。

2、原子类解决非原子性:
先介绍原子类的几个类和其中AtomicInteger的几个方法:
首先他是JDK1.5开始提供的原子类的操作:

 1). java.util.concurrent.atomic.AtomicInteger:对int变量进行原子操作的类。
 2). java.util.concurrent.atomic.AtomicLong:对long变量进行原子操作的 类。
3). java.util.concurrent.atomic.AtomicBoolean:对boolean变量  进行原子操作的类。

介绍一下AtomicInteger类:

构造方法:
public AtomicInteger():  初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer

成员方法:
int get():  获取AtomicInteger对象中存储的int值
int getAndIncrement(): i++   以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet(): ++i    以原子方式将当前值加1,注意,这里返回的是自增后的值。

代码如下:
测试类代码Demo06AtomicInteger:

public class Demo06AtomicInteger {
    public static void main(String[] args) throws InterruptedException {
        ThreadAtomicInteger atomicInteger = new ThreadAtomicInteger();
        Thread thread = new Thread(atomicInteger);
        thread.start();
        for (int i = 0; i < 10000; i++) {
            ThreadAtomicInteger.money.getAndIncrement();//自增
        }
        System.out.println("主线程睡眠两秒,等待所有线程执行完毕!");
        Thread.sleep(2000);
        System.out.println(ThreadAtomicInteger.money);
    }
}

ThreadAtomicInteger代码:

public class ThreadAtomicInteger implements Runnable{
    public static AtomicInteger money = new AtomicInteger();
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            money.getAndIncrement();//自增
        }
    }
}

截图如下:

在这里插入图片描述

由上图可知,每个线程都给变量money增加了10000,最终结果也正确,为什么AtomicInteger可以解决原子性呢,因为它应用到了CAS(乐观锁)机制,CAS的原理是Compare And Swap(比较并替换);当两个线程同时访问同一个值,并改变它的时候,乐观锁的作用就是在线程改变它的值之前,会与这个变量进行比较,如果这个变量被其他的线程改变了,那么他会更改这个值,在进行改变,确保改变时的值与变量一致。

AtomicInteger同样可以解决不可见性,和无序性,但是一般情况下,volatile解决不可见性和无序性比较方便,解决这两个问题一般不用AtomicInteger。

3、synchronized关键字解决原子性。

synchronized和AtomicInteger的区别:
synchronized解决一段代码的原子性;而AtomicInteger只能解决一个变量的原子性;

Synchronized解决非原子性的方法:
方法一:(同步代码块的方式)
示例代码如下:
DemoSynchronized测试代码:

public class DemoSynchronized {
    public static void main(String[] args) {
        MySynchronized ms = new MySynchronized();
        Thread thread1 = new Thread(ms);
        Thread thread2 = new Thread(ms);
        Thread thread3 = new Thread(ms);
        thread1.start();
        thread2.start();
        thread3.start();
        while(MySynchronized.tick > 0){
            synchronized(MySynchronized.class){
                if(MySynchronized.tick > 0){
                    System.out.println(Thread.currentThread().getName() + "线程正在卖第" + MySynchronized.tick + "张票");
                }
                MySynchronized.tick--;
            }
        }
    }
}

MySynchronized代码:

public class MySynchronized implements Runnable {
    public static int tick = 20;
    @Override
    public void run() {
        while(MySynchronized.tick > 0){
            synchronized(MySynchronized.class){
                if(MySynchronized.tick > 0) {
                    System.out.println(Thread.currentThread().getName() + "线程正在卖第" + MySynchronized.tick + "张票");
                }
                MySynchronized.tick--;
            }
        }
    }
}

方法二:(使用同步方法)
示例代码如下:
DemoSynchronized测试代码:

public class DemoSynchronized {
    public static void main(String[] args) {
        MySynchronized ms = new MySynchronized();
        Thread thread1 = new Thread(ms);
        Thread thread2 = new Thread(ms);
        Thread thread3 = new Thread(ms);
        thread1.start();
        thread2.start();
        thread3.start();
        MySynchronized.shopTick();
    }
}


MySynchronized代码:

public class MySynchronized implements Runnable {
    public static int tick = 20;
    @Override
    public void run() {
       shopTick();
    }
    public static synchronized void shopTick(){
        while(MySynchronized.tick > 0){
                if(MySynchronized.tick > 0) {
                    System.out.println(Thread.currentThread().getName() + "线程正在卖第" + MySynchronized.tick + "张票");
                }
                MySynchronized.tick--;
        }
    }
}


代码截图:
在这里插入图片描述
方法三:(使用Lock锁)
示例代码如下:
DemoSynchronized测试代码:

public class DemoSynchronized {
    public static void main(String[] args) {
        MySynchronized ms = new MySynchronized();
        Thread thread1 = new Thread(ms);
        Thread thread2 = new Thread(ms);
        Thread thread3 = new Thread(ms);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}


MySynchronized代码:

import java.util.concurrent.locks.ReentrantLock;

public class MySynchronized implements Runnable {
    public static int tick = 20;
    private ReentrantLock l = new ReentrantLock();
    @Override
    public void run() {
        while(MySynchronized.tick > 0){
            l.lock();
                if(MySynchronized.tick > 0) {
                    System.out.println(Thread.currentThread().getName() + "线程正在卖第" + MySynchronized.tick + "张票");
                }
                MySynchronized.tick--;
                l.unlock();
        }
    }
}



在这里插入图片描述

上图可知,这样就解决了多人卖票,卖重复票的问题了。即解决了多线程的非原子性问题。
Sychronized和Lock锁都是采用了悲观锁技术;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值