------------------------------------------------- 线程-----------------------------------------------------------
- 进程与线程的概念:
- 并发与并行;
- 多线程的创建(两种方式);
- 高并发问题;
- 解决不可见性、无序性、非原子性的方法;
一、进程和线程的概念
进程
进入到内存中的程序叫做进程
线程
线程是进程的一个执行单元,负责当前进程中程序的执行,一个进程中至少含有一个线程;(进程中只有一个线程,这种进程叫做单线程程序;进程中含有两个及以上的线程个数,这种进程叫做多线程程序)
引入到我们的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锁都是采用了悲观锁技术;