进程和线程
进程和线程的关系
- 进程包含线程,一个进程可以有一个线程(主线程),也可以有多个线程,每个线程拥有一份PCB对象
- 进程是操作系统分配资源的基本单位,线程是操作系统调度的基本单位.
- 每个进程内拥有自己独立的虚拟地址空间和独立的文件描述符表,而一个进程里的所有线程公用进程里的资源,所以同一个进程里的线程会互相影响.
为什么会出现线程
首先 并发编程(1.单核cpu发展遇到瓶颈,为了提高算力,需要多核cpu,就用到了并发编程 2. 有些任务需要"等待IO",在这期间,可以利用并发编程去做其他的事情) 成为刚需, 其次 虽然进程也能实现并发编程,但是线程比进程更加轻量:
- 创建线程比创建进程更快
- 销毁线程比销毁进程更快
- 调度线程比调度进程更快
- 使用多线程就是没有了操作系统为进程分配资源的时间
java线程和操作系统线程的关系
操作系统内部实现了线程这样的机制,并对用户层提供了API进行使用,而java的Thread类可以认为就是对操作系统的API进行了进一步的抽象和封装
第一个多线程代码
通过自己的MyThread继承Thread类
class MyThread extends Thread{
//重写run 明确新创建的线程干啥活
@Override
public void run() {
while(true){
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
System.out.println("main");
Thread thread = new MyThread();
thread.start();//真正开始创建线程(在操作系统内核中,创造出对应的PCB,PCB加入系统链表,参与调度)
}
}
第一个并发编程代码
class MyThread extends Thread{
//重写run 明确新创建的线程干啥活
@Override
public void run() {
while(true){
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
System.out.println("main");
Thread thread = new MyThread();
thread.start();//真正开始创建线程(在操作系统内核中,创造出对应的PCB,pcb加入系统链表,参与调度)
while(true){
System.out.println( "hello main");
Thread.sleep(1000);
}
}
}
我们可以看到,进程由main主线程进入,然后main线程和thread交替进行.
(但是在阻塞一秒后,先唤醒main线程还是thread线程是不确定的,操作系统对线程的调度是随机性的)
创建线程的多种方法
1.继承Thread类
第一就是上文中创建类继承Thread类,并重写run方法
2.实现Runnable接口
第二种就是实现Runnable接口,并实现其中的run方法,
Runnable接口是个函数式接口,其中只有一个run方法
class MyRnnnable implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hello myrunnable");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
//解耦合
//任务内容 和 线程本身 分离开
Runnable runnable = new MyRnnnable();//创建的runnable定义了任务
Thread t = new Thread(runnable);//把任务交给thread
t.start();//start创建具体线程
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
这样的优点是提高了解耦合度,重写的run方法只是定义了任务内容,然后通过Runnable把描述好的任务交给Thread实例,t.start()完成了线程的创建.
3.匿名内部类
匿名内部类可以说完成了三步: 1.继承 2. 方法的重写 3. 类的实例化
3.1 匿名继承Thread类
public static void main(String[] args) {
//匿名内部类
//继承 方法重写 实例化
//只是准备好了 还是需要start创建 因为线程还是需要操作系统创建
Thread t = new Thread(){
@Override
public void run() {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t.start();
}
3.2 匿名Runnable接口
public static void main(String[] args) {
//两步实现
/*Runnable runnable = new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread = new Thread(runnable);
thread.start();*/
// 一步实现
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
4. lambda表达式
public static void main(String[] args) {
//lambda 表达式
Thread t = new Thread(() -> {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
通常选择 Runnable 来写更好一些,来完成更好的解耦.写代码注重 高内聚,低耦合.Runnable 单纯的只是描述了一个任务.
多线程节约时间
我们利用单线程和多线程来计算两个变量的自增,从0自增到20亿,并记录两个线程各自的时间来完成比较. 不过,此代码的时间戳在main线程中,为了完成多线程时间的记录,我们需要在main线程中调用t1.join和t2.join 来让main线程等待t1和t2线程执行完毕.
public class Demo {
private static final long COUNT = 20_0000_0000;
private static void serial(){
//把方法执行时间记录
//记录当前的毫秒时间
long beg = System.currentTimeMillis();
int a = 0;
for(long i = 0; i < COUNT; i++){
a++;
}
a = 0;
for(long i = 0; i < COUNT; i++){
a++;
}
long end = System.currentTimeMillis();
System.out.println("单线程消耗时间: " + (end - beg) +" ms");
}
private static void concurrency(){
long beg = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
int a = 0;
for (long i = 0; i < COUNT; i++) {
a++;
}
});
Thread t2 = new Thread(() ->{
int a = 0;
for(long i = 0; i < COUNT;i++){
a++;
}
});
t1.start();
t2.start();
try{
t1.join();
t2.join();
}catch(InterruptedException e){
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("双线程执行事件:" + (end - beg)+ " ms");
}
public static void main(String[] args) {
concurrency();
serial();
}
}
由此我们可以见到多线程确实对时间完成了优化,那么为什么不是完全的优化一半时间呢? 这是因为线程的创建也需要占用时间.
Thread类的属性和方法
创建线程对象的同时命名
利用Thread的构造方法可以对线程进行命名,如下将线程命名为 张三
public static void main(String[] args) {
Thread t = new Thread(() ->{
while(true){
System.out.println("Thread1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"Thread");
t.start();
}
使用jconsole.exe来查看java进程
打开jdk文件夹下bin包中的jconsole.exe
右键以管理员身份运行,此时就可以看到正在运行的进程
点击连接后,大胆的选择不安全的连接
之后就可以得到我们创建的线程的信息
isDaemon 是否为后台程序
线程分为后台线程和前台线程,进程在执行完所有的前台线程后就会退出进程,而不管后台线程是否执行完毕.
线程启动 start
start决定了一个线程真正的启动,而run方法只是定义了线程要完成的任务
public class Demo8 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello MyThread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "MyThread");
t.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
如此可见 start启动线程后 ,t线程和main线程随机执行
但是如果只调用run方法的话
public class Demo8 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello MyThread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "MyThread");
t.run();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
可见只调用run方法的话,只是在main线程中调用了一个方法,并没有启动新的线程.
中断线程
在线程执行过程中,如果发生问题我们可以中断线程
手动设置标志位控制线程结束
设置isQuit变量来控制线程的结束
private static boolean isQuit = false;
public static void main(String[] args) {
Thread t = new Thread(() ->{
while(!isQuit){
System.out.println("线程运行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("新线程执行结束");
});
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("控制新线程退出");
isQuit = true;
}
使用Thread内置的标志位
1.Thread.interrupted() 是Thread类的静态方法 它的返回值是boolean, 他在默认情况下返回值是false,当在使用 thread 对象的 interrupted() 方法通知线程结束时,Thread.interruped的标志位变成true,然后在变回false.
2.Thread.currentThread().isInterrupted() 是Thread的实例方法, Thread.currentThread()返回的是当前Thread类的实例.
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
while(!Thread.currentThread().isInterrupted()){//获取到当前线程的实例 判断内置的标志位
System.out.println("线程运行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//e.printStackTrace();
System.out.println("即将退出");
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("退出");
//break;
}
}
});
t.start();
Thread.sleep(5000);
System.out.println("控制新线程退出");
t.interrupt();//对线程内部进行操作,若处于sleep阻塞状态,则会以 InterruptedException 异常的形式通知,清除中断标志,否则是对内置标志位进行操作
}
当出现InterruptedException是,是否结束线程取决于catch()内部的写法,可以忽略,也可以选择结束
jion线程等待
多个线程之间的执行顺序是不一定的,我们无法彻底决定调度器调度哪个线程,但是我们可以使用一些手段,来决定谁是先执行的,使用join()实例方法
哪个线程调用的join,哪个线程就会优先执行,一直到调用join线程的run方法执行完毕
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
System.out.println("Hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.getState());
t.start();
t.join();
System.out.println(t.getState());
}
线程的状态
java中线程的状态是一个枚举类型
- NEW:任务分配完毕,但是线程还未启动的,即还未调用start方法的
- TERMINATED:工作完成了的,即线程的run方法执行完毕
- RUNNABLE:可工作的,即就绪的线程,为正在工作中或马上要进行工作的线程
- TIMED_WAITING:调用sleep()进入排队
- BLOCKED:表示当前线程在等待索,也是在排队
- WAITING:线程等待被唤醒状态,
线程的安全
如果多线程代码运行的效果符合我们的预期想法,即在单线程下运行的效果,我们就认为线程是安全的
引起线程不安全主要有以下几个原因:
1.因为线程是抢占式执行,随机调度的,这是造成线程不安全的主要原因
2.多个线程对同一个变量进行修改操作
3.操作方式不是原子性的,会造成线程不安全
4.内存可见性问题(可见性指,一个线程对共享变量进行多次修改时,其他线程能及时的读取到修改后的变量的值.)
5.指令重排序:其是指在一段代码中,JVM、CPU指令集会对其进行优化,从而达到时间的更好地效率,这在多线程的情况下很可能使逻辑和之前不对等.
操作方式非原子的线程不安全
我们对同一个共享变量,使用两个线程对其分别进行50000次增加,最后来看一下变量的数值
class Counter{
public int count;
public void increase(){
count++;
}
}
public class Demo13 {
public static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
counter.increase();
}
});
Thread t2 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count: "+ counter.count);
}
}
由此我们可以看到结果并不是我们预想的10万,这就是出现了线程安全的问题,
但是为什么是这样呢?
原因
这是因为count++ 其实是三个指令:
1.将内存中的count值读取到CPU的寄存器中
2.CPU的寄存器完成 +1 的运算
3.将寄存器中count的值重新读取到内存中
但是因为抢占式执行,随机调度的原因,两个线程的三个步骤执行可能会有很多种情况,
在这种情况时,t1线程 load 后,一个寄存器中count的值为0,
t2 load add save后,另一个寄存器中count的值为1,内存中count的值也变为1,
此时t1线程 add save后 寄存器中的count值为1,内存中count的值还为1,
这时,两个线程分别进行了count的自增,但是count的值为1.
这就出现了线程不安全的问题
解决
像这种情况,我们就需要加锁()来解决问题, 通过synchronized加锁,使代码的执行变成原子的(就是整体的 将加锁中的代码完全执行完,才可以变换为令一个线程)
class Counter{
public int count;
public synchronized void increase(){
count++;
}
}
public class Demo13 {
public static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
counter.increase();
}
});
Thread t2 = new Thread(() ->{
for(int i = 0; i < 50000; i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count: "+ counter.count);
}
}
synchronized 用法
synchronized的意思使"同步",其在不同的环境中有不同的意义,在多线程的环境下,"同步"的意义就是互斥,即一个在进行时,另一个无法插入.
我们称synchronized为对象的synchronized,某个线程执行到一个对象的synchronized时,另一个线程执行到同一个对象的synchronized就会阻塞等待.
java的 锁 是存在在对象头里的
针对每一把锁,操作系统内部都维护了一个等待队列,当某个锁被一个线程占有时,其他的线程想要进行加锁操作就加不上了,从而进入了阻塞等待的状态,一直到之前被锁住的线程执行完后,操作系统在调度其他等待锁的线程
synchronized是 可重入锁 的,即当同一个线程对同一个锁对象进行二次执行,是可以允许完成的.
如下图,两个increase都是对 this 对象加锁的
class Counter1{
public volatile static int counter;
public synchronized void increase1(){
counter++;
}
public synchronized void increase2(){
counter++;
}
}
public class Demo26 {
public static Counter1 counter = new Counter1();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
counter.increase1();
counter.increase2();
});
t1.start();
t1.join();
System.out.println(counter.counter);
}
}
synchronized 使用示例
- synchronized直接修饰普通方法 锁的是synchronized对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
- synchronized修饰类方法 锁的是synchronized的类对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
- 修饰代码块: 需要明确指定锁哪个对象
锁当前对象:
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
锁类对象
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
两个线程竞争竞争同一把锁才会产生竞争,两个线程分别尝试获取不同的锁不会产生竞争.
内存可见性导致的线程不安全
可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.
- 当线程要读取一个共享变量的时候, 会先把变量从内存拷贝到cpu的寄存器中, 再从寄存器读取数据.
- 当线程要修改一个共享变量的时候, 也会先修改寄存器中的副本, 再同步回内存
由于每个线程有自己的cpu寄存器位置, 这些寄存器中的内容相当于同一个共享变量的 “副本”. 此时修改线程1的寄存器中的值, 线程2 的寄存器不一定会及时变化.
- 初始情况下, 两个线程的工作内存内容一致.
- 一旦线程1 修改了 a 的值, 此时内存不一定能及时同步. 对应的线程2 寄存器中的 a 的值也不一定能及时同步.
static class Counter{
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() ->{
while(counter.flag == 0){
}
System.out.println("循环结束");
});
Thread t2 = new Thread(() ->{
Scanner scanner = new Scanner(System.in);
System.out.println("请输入flag的值");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
当flag的值修改后,循环并没有立刻退出
但是,在加入volatile后.就可以保证内存可见性
但是使用 synchronized 关键字加锁。不光保证原子性,还能保证内存可见性。被 synchronized 包裹起来的代码,编译器就不敢轻易的做出上面优化的那种操作。
wait 和 notify
wait和notify分别指阻塞和通知停止阻塞,更倾向于处理线程调度随机性的问题.
wait必须在加锁的代码块中使用,
wait 内部会做三件事:1、先释放锁 2、等待其他线程的通知 3、收到通知之后,重新获取锁,并继续往下执行。
public static void main1(String[] args) throws InterruptedException {
Object object = new Object();
//wait 哪个对象,就得针对哪个对象加锁。
synchronized (object) {
System.out.println("wait 前");
object.wait();
System.out.println("wait 后");
}
}
public static Object locker = new Object();
public static void main(String[] args) {
Thread waitTask = new Thread(() ->{
synchronized (locker){
try{
System.out.println("wait 开始");
locker.wait();
System.out.println("wait 结束");
}catch(InterruptedException e){
e.printStackTrace();
}
}
});
waitTask.start();
Thread notifyTask = new Thread(() ->{
Scanner scanner = new Scanner(System.in);
System.out.println("请输入任意内容");
scanner.next();
synchronized (locker){
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
});
notifyTask.start();
}
notifyAll
假如一个锁对象有10个阻塞的线程,notify只会随机唤醒一个线程,而notifyAll则会唤醒所有阻塞的线程,但是仍处于随即调度状态.