启动线程的2种方法
psvm(){
/*方法1*/
Thread t1 = new Thread(){
int num;
@Override
public void run() {
//process
}
};
t1.start(); //只能启动一次
t1.start(); //报错!!!
Thread t2 = new Thread(){
int num;
@Override
public void run() {
//process num++;
}
};
t2.start();
/*方法2*/
Runnable r1 = new Runnable() {
int num;
@Override
public void run() {
//process num++;;
}
};
new Thread(r1).start(); //可以new多个线程启动多次
new Thread(r1).start(); //new出来一个新线程,可以再启动
}
法1 线程t1只能启动1次,要想再启动就要创造新的线程t2,且每次启动新的线程对num的操作都是局部的,
法2 中把Runable对象封装到多个线程中启动, 则对num的操作是全局的
- Thread
- 类 只能单继承,不够灵活
- Thread 每次只能启动一次 ,对象就只能用在一个线程中
- Runnable
- 接口 可以多实现,比较灵活
- 不依赖启动方法,一个对象被封装到多个Thread对象中启动
匿名内部类
前文是匿名内部类写法
改写成 正常写个类,再new个对象 :
class ThreadAddNum extends Thread{
int num;
@Override
public void run(){
for(int i = 0; i < 10000; i++){
num++;
}
}
}
class RunnableAddNum implements Runnable {
int num;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
}
public class thread_test {
public static void main(String[] args) {
ThreadAddNum t1 = new ThreadAddNum(), t2=new ThreadAddNum();
t1.start();
t2.start();
RunnableAddNum r1 = new RunnableAddNum();
new Thread(r1).start();
new Thread(r2).start();
}
}
线程安全问题
原子性 与 synchronized
稍微改动下前文的代码
class ThreadAddNum extends Thread{
int num;
@Override
public void run(){
for(int i = 0; i < 10000; i++){
num++;
}
}
}
class RunnableAddNum implements Runnable {
int num;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
}
public class thread_test {
public static void main(String[] args) {
ThreadAddNum t1 = new ThreadAddNum(), t2=new ThreadAddNum();
t1.start();
t2.start();
RunnableAddNum r1 = new RunnableAddNum();
Thread rt1 = new Thread(r1) , rt2=new Thread(r1);
rt1.start();
rt2.start();
//等待rt1,rt2两个线程结束
try {
rt1.join();
rt2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(r1.num);
}
}
功能是一样的 ,只是把用runnable启动的线程改变后的num输出
发现每次num结果不一样.并且一般都不是20000.
按我们理解来说,答案应该是2w,问题出在哪里???
问题在于 2个线程并行的对num操作,且改变num的过程是写读num,再++,再写回.
如此会导致可能线程1,2同时读了num++再同时写回,就相当于只加了1次,或者线程1读了num,还没写回,线程2又去读num,然后线程1写回,2写回,线程2写回的结果覆盖了线程1,又相当于只加了1次…
引出了 线程不安全:
- 线程不安全:
可以有多个线程同时操作一个资源时没有受到管制,会线程不安全- 解决方法: 对于资源的操作原子性保证
- 原子性: 一口气完成,不会中断…资源的每次操作不会影响到其他线程读取的结果
synchronized
协调,同步 之意
synchronized用法
synchronized原理
这里改写一下RunnaleAddNum类就行,下面列举了3个改写方法:
//法1, synchronized修饰方法
class RunnableAddNum implements Runnable {
int num;
@Override
synchronized public void run() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
}
//法2,synchronized(Object)修饰代码块,object用 this
class RunnableAddNum implements Runnable {
int num;
Object l = new Object();
@Override
synchronized public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (this){
num++;
}
}
}
}
//法3,synchronized(Object)修饰代码块,object用l
class RunnableAddNum implements Runnable {
int num;
Object l = new Object();
@Override
synchronized public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (l){
num++;
}
}
}
}
可见性 与 volatile
class RunnbaleVt implements Runnable{
//volatile boolean flag = true;
boolean flag = true;
@Override
public void run(){
System.out.println ("启动线程.....");
while(flag){
// System.out.print("");
}
System.out.println ("执行任务完成.....");
}
}
public class thread_test {
//主线程
public static void main(String[] args) {
RunnbaleVt rtv = new RunnbaleVt();
//启动线程1
new Thread(rtv).start();
//启动线程2
new Thread() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
rtv.flag=false;
System.out.println(rtv.flag);
System.out.println("rtv.flag=false;");
}
}.start();
}
}
分析一下这段代码, 线程1开启了RunnbaleVt,会执行循环,flag=false时跳出循环,打印"执行任务完成"
然后开启线程2更改了false值,但执行发现线程1始终不会跳出循环.
解释
- 因为对象都在堆内存(主存)中,堆内存是被各线程共享,
- 而线程1,2分别有私有的栈空间,
- 由于现在的cpu多核,不同的线程在不同的cpu上运行,有不同的cache系统,由于缓存机制,线程第一次从堆内存中读了flag后,就会缓存在各自的cache中,之后的读写都是与自己的cache进行.
故线程2更新的flag不会被线程1读.- 这就是可见性问题. 可以用volatile解决. (见图)
volatile
总结出几点关键的:
- volatile 关键字是用来修饰变量的
- 通过 volatile 修饰的变量,所有关于该变量的读操作,都会直接从主内存中读取,而不是 CPU 自己的缓存。而所有该变量的写操都会写到主内存上
- 主要解决2个:
- 多线程间可见性的问题,
- CPU 指令重排序的问题
- Java volatile Happens-Before 规则保证了 指令不会乱序发射,且使得 volatile能有更多的效果:
- 当 Thread A 修改了某个被 volatile 变量 V,另一个 Thread B 立马去读该变量 V。一旦 Thread B 读取了变量 V 后,不仅仅是变量 V 对 Thread B 可见, 所有在 Thread A 修改变量 V 之前 Thread A 可见的变量,都将对 Thread B 可见。
- 当 Thread A 读取一个 volatile 变量 V 时,所有对于 Thread A 可见的其他变量也都会从主内存中被读取。
- 不能保证原子性
- 效率方面:
如果大家了解 CPU 的多级缓存机制,(不了解应该也能猜到),从主内存读取数据的效率一定比从 CPU 缓存中读取的效率低很多。包括指令重排序的目的也是为了提高计算效率,当重排序机制被限制时,计算效率也会相应收到影响。因此,我们应该只在需要保证变量可见性和有序性时,才使用 volatile 关键字。