多线程
引入:多线程在我们网络生活中及其常见,例如边打游戏边听歌,在同一时间断执行多个代码,在程序中就要使用多线程。
首先我们要先了解几个概念:什么是并发,什么是并行,什么是进程,什么是线程。
并行: 指两个或多个事件在同一时刻发生(同时执行)。
并发: 指两个或多个事件在同一个时间段内发生(交替执行)。
进程: 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程: 是进程中的一个执行单元或者叫控制单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
例如:我们的main方法就是一个线程,它有它特定的名称叫做(main线程或者主线程)。
那么JVM虚拟机启动时有几个线程呢?
我的回答是两个:一个是main线程,一个是垃圾回收线程(GC)。
了解这几个概念后,那么久让我们看看如何创建一个线程,线程的创建方式有两种:
1.继承Thread类,重写run方法。
2.实现Runnable接口,重写run方法。
Thread方式:
首先我们要看Thread中有哪些构造方法和那些成员方法供我们使用
构造方法:
public Thread() :空参构造
public Thread(String name) :创建对象时指定线程名称
public Thread(Runnable target) :创建Thread对象并传入一个实现Runnable接口的类
public Thread(Runnable target,String name) :创建Thread对象并传入一个实现Runnable接口的类并指定线程名称。
成员方法:这些方法在下面的例子中我们会用到
public String getName() :获取当前线程名称。
public void start() :开启当前线程,JVM默认调用线程中的run方法。
public void run() :线程要执行的代码。
public static void sleep(long millis) :让线程睡多久,传入毫秒值。
public static Thread currentThread() :返回当前线程的引用。
继承Thread方式创建一个线程类:
//自定义线程类
public class MyThread extends Thread {
//继承Thread类重写run方法
@Override
public void run() {
//run方法里面是我们自己编写线程要执行的代码。
System.out.print("我是通过继承Thread创建的线程!");
}
}
//测试类
public class ThreadTest {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); //start()方法是开启线程,后面会说到
}
}
测试结果:
实现Runnable方式创建一个线程类:
//自定义一个线程类 实现Runnable的方式
public class MyThread implements Runnable {
@Override
public void run() {
System.out.print("我是通过实现Runnable接口创建的线程!");
}
}
public class ThreadTest {
public static void main(String[] args) {
//创建线程类对象
MyThread mt = new MyThread();
//创建Thread类,传入继承了Runnable接口的线程类
Thread t = new Thread(mt);
t.start(); //start()方法是开启线程,后面会说到
}
}
测试结果:
到这里我相信大家一定想问使用多线程的好处是啥?
(单核情况下)多线程的好处就是可以让多个线程同时执行,多个线程抢用一个CPU资源,达到多个程序同时运行的效果。
现在我们看一下多线程的运行效果:
//自定义一个线程类 实现Runnable的方式
public class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//Thread.currentThread().getName()获取当前线程的名称
System.out.println(Thread.currentThread().getName() + "..." + i);
}
}
}
//测试类
public class ThreadTest {
public static void main(String[] args) {
//创建线程类对象
MyThread mt = new MyThread();
//创建Thread类,传入继承了Runnable接口的线程类
Thread t = new Thread(mt);
//启动线程
t.start();
//注意:这个循环是在main方法中执行的,它属于main线程
for (int i = 0; i < 100; i++) {
//Thread.currentThread().getName()获取当前线程的名称
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
测试结果:
上述两种方式我们创建线程还需要另外创建一个线程类,比较麻烦。
下面介绍一下匿名内部类的方式:
public class ThreadTest {
public static void main(String[] args) {
//第一种,直接new Thread (常用)
//往构造方法中传入一个字符串就是给Thread创建的时候设置一个名称
new Thread("***"){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//Thread.currentThread().getName()获取当前线程的名称
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}.start();
//第二种,new Thread 并往里面传入一个Runnable接口 (基本不用传入Runnable不是多此一举吗?)
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//Thread.currentThread().getName()获取当前线程的名称
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}).start();
}
}
测试结果:
sleep方法:
public class ThreadTest {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
try {
//每循环一次暂停一秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
}
测试结果:
(面试题)(可能面试会问到哦!)
继承Thread和实现Runnable接口有什么好处与坏处呢?
如果你已经继承Thread类 就不能再继承其他类了 (因为一个类只能继承一个类)
而实现接口可以实现多个 (而一个类可以实现多个接口)
避免了单继承的局限性,减少了类域类之间的依赖,降低了耦合度。
所以说在使用多线程的时候还是建议使用实现Runnable接口的方式。
多线程的高并发问题及线程安全:
高并发:是指在某个时间点上,有大量的用户(线程)同时访问同一资源。例如:天猫的双11购物节,12306的在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况。
线程安全:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现"数据污染"的问题。
多线程的运行机制:
当一个线程启动后,JVM会为其分配一个独立的"线程栈区",这个线程会在这个独立的栈区中运行。
看一下简单的线程的代码:
多个线程在各自栈区中独立、无序的运行,当访问一些代码,或者同一个变量时,就可能会产生一些问题
多线程的安全性问题-可见性
例如下面的程序,先启动一个线程,在线程中将一个变量的值更改,而主线程却一直无法获得此变量的新值。
public class MyThread extends Thread {
public static int num = 0;
@Override
public void run() {
System.out.println("线程开启!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println("线程完结!");
}
}
public class Work1 {
public static void main(String[] args) {
new MyThread().start();
while (true){
if (MyThread.num == 1){
break;
}
}
System.out.println("main结束!");
}
}
运行结果:因为while循环里面用的副本num无法得到更新,所以他还是0,会一直进入死循环。main方法无法正常结束。
多线程的安全性问题-有序性
有些时候“编译器”在编译代码时,会对代码进行“重排”,例如:
int a = 10; //1行
int b = 20; //2行
int c = a + b; //3行
第一行和第二行可能会被“重排”:可能先编译第二行,再编译第一行,总之在执行第三行之前,会将1,2编译
完毕。1和2先编译谁,不影响第三行的结果。
但在“多线程”情况下,代码重排,可能会对另一个线程访问的结果产生影响:
多线程的情况下 ,我们是不希望对代码进行排重的。
多线程的安全性问题-原子性
public class MyThreadWork2 extends Thread {
public static int num = 0;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
}
public class Work2 {
public static void main(String[] args) {
new MyThreadWork2().start();
for (int i = 0; i < 10000; i++) {
MyThreadWork2.num++;
}
System.out.println(MyThreadWork2.num);
}
}
运行结果:
结论:按照常理来讲最终结果应该是20000,但是这里的结果却不是,我们连续运行几次发现也不是20000,那么到底是为什么呢,因为num作为public static 所修饰的共享变量,在进行++时,如果说0线程目前+到了4002,当CPU把执行权给了main线程以后那么它会从4002开始+,当main中的线程执行完毕后,就会输出num信息 ,然后main线程结束,此时0线程还没有+完,所以输出的值就不是20000,虽然后续0线程还会继续给num++;但是main线程已经结束,不会再输出了,所以控制台打印的就是不到20000的数字。
所以说两个线程访问同一个变量num的代码不具有"原子性
如何解决呢
下面就会有volatile和原子类出场。
先说volatile,它可以用于修饰变量,当共享变量被修饰后就不会出现,可见性和有序性问题了。
这个比较简单就不在演示了。
原子类:基本类型:他们的基本实现是基于CAS(乐观锁)的机制实现的,下一篇会说到。
1).java.util.concurrent.atomic.AtomicInteger:对int变量操作的“原子类”;
2).java.util.concurrent.atomic.AtomicLong:对long变量操作的“原子类”;
3).java.util.concurrent.atomic.AtomicBoolean:对boolean变量操作的“原子类”;
它们可以保证对“变量”操作的:原子性、有序性、可见性。
成员方法:
get() 获取当前值。
getAndIncrement() 相当于i++;
incrementAndGet() 相当于++i;
addAndGet(int参数) 相当于当前值与传入的值相加;
getAndSet(int参数) 返回的参数是旧值,参数是新值。
getAndSet演示:
AtomicInteger ai = new AtomicInteger(12);
int num = ai.getAndSet(88);
//最终的值是
num = 12;
ai = 88;
基本使用:
public class MyThread extends Thread {
public static AtomicInteger ai = new AtomicInteger();//空参就是0,传入的是几就是几
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
ai.getAndIncrement();//先获取再自增;类似于a++;
}
}
}
public class Work1 {
public static void main(String[] args) {
new MyThread().start();
for (int i = 0; i < 10000; i++) {
MyThread.ai.getAndIncrement();//先获取再自增;类似于a++;
}
try {
Thread.sleep(1000); //睡一秒 防止主线程一下执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(MyThread.ai.get());
}
}
运行结果:可以看到,问题得到了解决。
数组类型:
1).java.util.concurrent.atomic.AtomicIntegetArray:对int数组操作的原子类。
2).java.util.concurrent.atomic.AtomicLongArray:对long数组操作的原子类。
3).java.utio.concurrent.atomic.AtomicReferenceArray:对引用类型数组操作的原子类。
演示:
好了,这里就介绍那么多多线程的基本使用,下面还会持续发布多线程解决高并发问题和锁,线程状态等高级部分的文章,请注意查看。