叨叨游戏网
您的当前位置:首页Java多线程(多线程基本操作,多线程安全问题等)

Java多线程(多线程基本操作,多线程安全问题等)

来源:叨叨游戏网
  • 一、创建线程四种方式

    • 1)继承Thread
  • 2)调用Runnable

  • 3)匿名内部类

  • 4)使用lambda表达式来创建

  • 二、了解Thread 类

    • 2.1Thread的常见的构造方法
  • 2.2Thread的几个常见的属性

  • 三、启动一个线程

  • 四、中断一个线程

    • 4.1 让线程的入口方法执行完毕
  • 4.2 使用Thread类提供的interrupt方法

  • 五、等待线程

  • 六、线程休眠

  • 七、线程的状态

  • 八、线程安全(重要)!!!

    • 8.1导致线程不安全的原因:
  • 8.2解决线程安全问题方法

    • synchronized
  • volatile

  • 九、对象等待集


一、创建线程四种方式

=============================================================================

1)继承Thread


利用多态机制,继承于Thread机制

**1)创建一个子类,继承于Thread

2)重写run 方法

3)创建子类实例

4)调用start 方法**

public class Demo {

//创建多线程

public static void main(String[] args) {

MyThread2 myThread2=new MyThread2();

myThread2.start();

}

}

class MyThread2 extends Thread{

@Override

public void run() {

//这里写线程要执行的代码

}

}

2)调用Runnable


通过实现Runnable 接口,把Runnable 接口的实例赋值给Thread

**1)定义Runnable接口的实现类

2)创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象

3)调用start () 方法**

public class Demo {

//创建多线程

public static void main(String[] args) {

//通过Runnable 接口来创建

Runnable myTask=new MyTask();

Thread t=new Thread();

t.start();

}

}

class MyTask implements Runnable{

@Override

public void run() {

//重写run()方法

}

}

Runnable 本质上还是要搭配Thread来使用,只不过和直接继承Thread相比,换了一种指定任务的方式而已

这两种方式中Runnable 方式更好一点,能够让线程本身,和线程要执行的任务,更加“解耦合”

3)匿名内部类


通过匿名内部类相当于继承了Thread,作为子类重写run()实现

public class Demo {

//创建多线程

public static void main(String[] args) {

//通过匿名内部类来实现

Thread t=new Thread(){

@Override

public void run() {

//重写run方法

}

};

t.start();

}

}

通过Runnable 匿名内部类来实现

public class Demo {

//创建多线程

public static void main(String[] args) {

//通过匿名内部类来实现

Thread t=new Thread(new Runnable() {

@Override

public void run() {

//重写run方法

}

});

t.start();

}

}

4)使用lambda表达式来创建


public class Demo {

//创建多线程

public static void main(String[] args) {

Thread t=new Thread(()->{

//编写线程代码

}

);

t.start();

}

}

()->{ }这个就是lambda表达式

二、了解Thread 类

===============================================================================

2.1Thread的常见的构造方法


| Thread() | 创建线程对象 |

| — | — |

| Thread(Runnable target) | 使用Runnable对象创建线程对象 |

| Thread(String name) | 创建线程对象并命名 |

| Thread(Runnable target,String name) | 使用Runnable对象来创建线程,并命名 |

2.2Thread的几个常见的属性


| ID | .getId() |

| — | — |

| 名称 | .getName() |

| 优先级 | .getPriority() |

| 状态 | .getState() |

| 是否后台线程 | .isDaemon() |

| 是否存活 | .isAlive() |

| 是否被中断 | .isInterrupted |

| 获取当前线程的实例 | currentThread() |

优先级和线程调度有关,由操作系统来完成。

后台线程,不影响整个进程的结束

前台线程,会影响到整个进程的结束

是否存活就是run()方法是否运行结束了

三、启动一个线程

===========================================================================

start

start 是Thread类的一个关键方法

功能:让操作系统内核真正创建一个线程来执行

start 和run 的区别

start 是创建线程(有新的执行流)

调用run只是一个普通的方法调用,不涉及创建新线程(仍然在原来的线程中,没有涉及到新的执行流)

调用strat 方法

public class Demo {

//start 和 run 的区别

public static void main(String[] args) {

MyThread2 myThread2=new MyThread2();

myThread2.start();

while(true){

System.out.println(“hehe”);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

class MyThread2 extends Thread{

@Override

public void run() {

while(true){

System.out.println(“haha”);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

可以看到两个线程并发执行,

而如果是调用run()方法,就是普通的调用,没有创建新线程,一直在循环里出不来

四、中断一个线程

===========================================================================

4.1 让线程的入口方法执行完毕


线程执行完毕,运行5S 线程执行完毕

public class Demo {

//中断一个线程

static boolean isRunning=true;

public static void main(String[] args) {

Thread t1=new Thread(){

@Override

public void run() {

while(isRunning){

System.out.println(“hello”);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

};

t1.start();

try {

Thread.sleep(5000);

} catch (InterruptedException e) {

e.printStackTrace();

}

isRunning=false;

System.out.println(“线程运行5S结束”);

}

}

4.2 使用Thread类提供的interrupt方法


针对上面的方式进行修改

1,把上面的while()中判断条件进行修改

2,把catch里面的代码,加一个break

调用 interrupt()是通知线程结束,具体还是看内部代码的实现

public class Demo {

//使用Thread 方法来中断线程

public static void main(String[] args) {

Thread t1=new Thread(){

@Override

public void run() {

while(!Thread.currentThread().isInterrupted()){

System.out.println(“hello”);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

// e.printStackTrace();

break;

}

}

}

};

t1.start();

try {

Thread.sleep(5000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(“线程结束”);

t1.interrupt();

}

}

五、等待线程

=========================================================================

我们在创建多个线程之后,每个线程都是一个的执行流~

这些线程每个线程的执行顺序都是不确定的,完全取决于操作系统的调度,这里的等待线程机制就是一种确定线程先后顺序方式,确定线程的结束顺序,无法确定谁先开始,可以确定谁先结束 使用join()

join 起到的效果就是等待某个线程结束,谁调用join就等待谁结束

通过代码来解释:

public class Demo {

//使用Thread 方法来中断线程

public static void main(String[] args) {

Thread t1=new Thread(){

@Override

public void run() {

for(int i=0;i<5;i++){

System.out.println(“hello”);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

};

t1.start();

try {

t1.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

这里在main 方法中调用了join,相当于在主线程中,等待t1线程结束

main方法在执行的时候,遇到join就会堵塞等待,一直等t1线程执行完毕,这个时候join才会继续往下执行

也就是说谁调用join 谁先结束

六、线程休眠

=========================================================================

当执行sleep时就是让线程休眠,所谓的休眠就是把线程的task struct放入等待队列

CPU在执行的时候是挑等待队列中的线程来执行的,而sleep的线程在等待队列不在就绪队列,所以不会被执行

也可以在sleep中加上时间,等时间过去后,等待队列的线程才有机会到就绪队列,至于什么时候到就绪队列还是要看调度器执行

等待队列可能有好多了,具体谁先出队列,先回到就绪队列和设定的时间相关,如果时间一样就看系统的调度

七、线程的状态

==========================================================================

在我们调试多线程程序有帮助

| NEW | Thread 对象刚创建,还没有在系统中创建线程,相当于任务交给了线程,但是线程还没有开始执行 |

| — | — |

| Runnable | 线程是一个准备就绪的状态,随时可能调度到CPU上执行,或者正在CPU上执行(线程的task struct在就绪队列中) |

| Blocked | 线程堵塞(线程在等待队列里)没有竞争到锁 |

| Waiting | 线程堵塞(线程在等待队列里)调用waiting 方法 |

| Timed_Waiting | 线程堵塞(线程在等待队列里)调用sleep方法 |

| Terminated | 线程结束了(Thread对象还没销毁) |

八、线程安全(重要)!!!

================================================================================

多线程修改同一变量

public class Demo {

//线程安全问题

static class Counter{

//创建一个自增类,通过线程调度来展示线程安全问题

public int count=0;

public void increase(){

count++;

}

}

public static void main(String[] args) {

//通过两个线程同时对count进行自增

Counter counter=new Counter();

Thread t1=new Thread(){

@Override

public void run() {

for(int i=0;i<50000;i++){

counter.increase();

}

}

};

Thread t2=new Thread(){

@Override

public void run() {

for(int i=0;i<50000;i++){

counter.increase();

}

}

};

t1.start();

t2.start();

try {

t1.join();

t2.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(counter.count);

}

}

此处代码就是t1,t2 两个线程修改同一变量,存在线程安全问题,正常情况下count值应该是100000,但是运行下来,count值是在50000~100000之间,每次都在变化,这是为什么呢? 这里就是触发了线程安全的问题

这里我们先看count ++ 具体做了什么事情,这里我们就要引入JMM了

JMM(JVM 实现方式的抽象,Java 程序和内存之间是如何交互的)

1)先把内存数据读取到CPU的寄存器中

2)针对寄存器中的内容,通过类似于ADD这样的指令进行+1,操作的结果仍然是放在寄存器中里

3)把寄存器中的数据,写回到内存中

由于多线程之间是抢占式执行的,可能第一个线程执行自增一半时,就可能被调度出CPU,由第二个线程再次自增

LOAD就是从内存中读取数据到寄存器 ADD就是进行自增效果 SAVE就是将寄存器中数据写回内存中

如果出现第一个线程读取到数据为0时,在进行自增操作时,线程二也进行读取数据,两个线程都读到的是0,相当于两次自增操作只增加了一次,只要是线程二的读取不是在线程一SAVE操作后,就会发生自增异常的情况!所以两个线程分别自增50000次,数据最后的数值是在50000~100000之间的。这就是抢占式执行同一个资源所带来的异常!

如果这里是两个CPU也是同样的情况,这样的不确定性,不符合预期的要求,就认为是BUG,因为我们执行代码就是追求的是确定性

因篇幅问题不能全部显示,请点此查看更多更全内容