多线程
并发与并行
并行:两个事件在同一时刻都在发生。
并发:两个事件在同一个时间段内都在发生。(交替执行)
进程与线程
进程:正在内存中运行的程序,称为进程。比如安装在计算机硬盘中的软件(QQ、360等),当要双击运行时,会在内存中运行起来。每个软件都有一个属于自己的进程。
线程:进程中用与执行某个功能的执行单位。线程属于某个进程,一个进程可以有多个线程,每个线程运行互不影响。
进程与线程的区别
线程是属于某个进程。
每一个进程有自己独立的内存空间(独立的栈、独立的堆),且至少有一个线程。
每个线程都会跟进程中申请一快独立的栈,共享进程的堆。
线程调度
线程调度是指CPU在不同的进程不同的线程之间进行快速切换。
一个单核CPU,在同一时刻只能执行某个进程中的某个线程。
线程调度的分类
分时调度:每个线程平均拥有CPU的执行权。
抢占式调度:每个进程随机分配CPU的执行权(具体的分配和线程的优先级有关)。
我们java程序(java进程)中所有的线程采用抢占式调度。
Thread类的介绍【重点】
我们右键运行的是一个java进程,该进程中默认至少有两个线程。
a、main方法所在的线程,称为主线程。
b、垃圾回收器线程。
我们是否可以再创建一个新的线程?
可以!!因为java已经把代表线程的类封装好了,Thread类。
Thread类的构造方法
public Thread();//无参构造,线程会有默认的名字 Thread-0。
public Thread(String name);//带有线程名字的构造。
public Thread(Runnable r);//带有线程任务的构造。
public Thread(Runnable r,String name);//既带有名字又带有任务的构造。
Thread类的成员方法
public String get Name();//获取线程名字
public void setName(String name);//修改线程名字。
public void run();//代表线程要执行的任务,任务有的代码要写在方法此方法中
public void start();//线程只创建并不会执行,必须要调用start开启后才会执行任务
public static void sleep(long millis);//让当前线程休眠多少毫秒
public static Thread currentThread();//获取当前线程对象
创建新的线程的两种方式【重点】
一、继承方式
创建步骤:
1、创建子类继承Thread。
2、子类中重写run方法(在run方法中编写线程要执行的任务代码)。
3、创建子类的对象(实际上就是创建一个线程对象)。
4、调用线程对象的start方法,启动该线程。
代码实现:
MyThread.java
//创建子类继承Thread
public class MyThread extends Thread{
//重写run方法,在run中编写要执行的代码
@Override
public void run() {
for (int i = 0; i <50 ; i++) {
System.out.println("子线程"+i);
}
}
}
ThreadTest.java
public class ThreadTest {
public static void main(String[] args) {
//创建子线程对象
MyThread myThread = new MyThread();
//启动该线程
myThread.start();
//主线程 不会等待子线程任务结束
for (int i = 0; i < 50; i++) {
System.out.println("主线程" + i);
}
}
}
注意:
我们可以给线程起名,也可以使用默认的名字。
我们获取线程的名字的时候,尽量使用通用方式( Thread currentThread().getNmae);如果是子线程内部,可以直接调用getName()。
二、实现方式
步骤:
1、创建实现类,实现Runnable接口(实际上接口中有任务方法,run方法)。
2、实现类重写run()方法(run中编写具体的任务代码)。
3、创建实现类对象(该实现类对象并不是线程对象,我们称为任务对象)。
4、创建Thread对象,同时传入实现类对象。
public Thread(Runnable r);//带有线程任务的构造
5、启动该线程(调用start方法)。
代码实现:
MyRunnable.java
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i <50 ; i++) {
System.out.println("子线程"+i);
}
}
}
MyRunnableTest.java
public class MyRunnableTest {
public static void main(String[] args){
//创建实现类对象(该实现类对象并不是线程对象,我们称为任务对象
MyRunnable myRunnable = new MyRunnable();
//创建Thread对象,同时传入实现类对象
Thread thread = new Thread(myRunnable);
//启动线程
thread.start();
//主线程不会等待子线程结束
for (int i = 0; i < 50; i++) {
System.out.println("主线程"+i);
}
}
}
三、两种方式的比较
实现方式比继承方式所具有的优势
1、实现方式线程和任务是分开的,是由程序员自己组合。
2、实现方式避免了java单继承的不足。
3、增加程序的健壮性,实现解耦操作。
4、对于线程池来说,我们需要的是Runnable的实现类,不需要Thread的子类。
匿名内部类简化线程创建方式【重点】
作用:可以快速创建一个类的子类对象或者一个接口的实现类对象。
代码实现:
TestDemo.java
public class TestDemo {
public static void main(String[] args) {
//1、继承方式创建线程
new Thread() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}.start();
//2、实现方式创建线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}).start();
//主线程不会等待子线程结束
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}
高并发及线程安全
高并发:是指在某个时间点上,有大量的用户(线程)同时访问统一资源。
线程安全:在某个时间点上,发生高并发后,访问的数据出现”不符合实际的数据“,称为线程安全有问题。
多线程的运行机制
当一个线程启动后,JVM虚拟机会为其分配一个独立的”线程栈区“,该线程会在这个独立的栈区运行。
代码实现:
MyThread.java
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("i=" + i);
}
}
}
Demo.java
public class Demo {
public static void main(String[] args) {
//创建两个线程对象
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
//启动线程
myThread1.start();
myThread2.start();
}
}
多线程安全问题——可见性
当一个共享的变量被多个线程使用时,其中某个线程对共性变量进行了修改,对于其他线程来说并不是立刻可见的。
先启动一个线程,在线程中将变量的值更改,而主线程却一直无法获得此变量的新值。
多线程安全问题——有序性
我们在写程序时,有时候”编译器“在编译代码的时候,会对代码进行重新排列,但是在”多线程“情况下,代码重新排列,可能会对另一个线程的访问的结果产生影响。
多线程安全问题——原子性
线程对一个共享变量,进行++时,这个++分两步操作,先取出值+1,然后给共享变量赋值。如果取出值+1后,还没有来得及赋值,被其他线程抢走CPU时,此时我们称为++操作不具有原子性。
volatile关键字
用来修饰成员变量(静态变量),被修饰的变量具有可见性和有序性。
volatile解决可见性
代码实现:
public class MyThread extends Thread {
//无论创建多个MyThread对象,他们共性一个静态变量a
public volatile static int a = 0;
@Override
public void run() {
System.out.println("线程启动,休息2秒...");
try { Thread.sleep(1000 * 2); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("将a的值改为1");
a = 1;
System.out.println("线程结束...");
}
}
public class TestSafaDemo01 {
public static void main(String[] args) {
//1.启动线程
MyThread t = new MyThread();
t.start();
//2.主线程继续
while (true) {
if (MyThread.a == 1) {
System.out.println("主线程读到了a = 1");
}
}
}
}
volatile解决有序性
volatile不能解决原子性
volatile关键字小结
1、解决变量的可见性,一旦变量发生改变,所有使用到该变量的线程都会取得最新值。
2、解决变量的有序性,一旦变量加上volatile,那么编译器不会改变代码的重排。
3、无法解决变量操作过程张原子性,对变量的操作还是有可能被其他线程打断。
原子类
是对普通类型(int、Integer)的原子类封装,使其操作成员原子操作。
作用
对原子类的增加或减少操作,保证是原子性,保证中间不会被其他线程打断。
注意:原子类既可以解决原子性,也可以解决有序性和可见性。
AtomicInteger类示例
1、是对int类型变量进行操作的原子类。
2、构造方法。
public AtomicInteger(int num);
3、成员方法。
public int getAndIncrement();//就相当于 变量++
public int incrementAndGet();//就相当于 ++变量
4、代码实现:
public class MyThread extends Thread {
public static AtomicInteger a = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a.getAndIncrement();//相当于 a++
}
System.out.println("修改完毕!");
}
}
public class TestSafeDemo {
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start(); //线程1 对a加了10000次
t2.start(); // 线程2 对a加了 10000次
Thread.sleep(1000);
System.out.println("获取a最终值:" + MyThread.a);
//总是不准确的。原因:两个线程访问a 的步骤不具有:原子性
}
}