线程:以解决线程安全问题为主
进程:运行时程序,操作系统分配内存资源的最小单位。
线程 :进程内部最小执行单元。
多线程的优点:提高程序响应速度,可以多个线程各自完成自己的工作,提高设备利用率。
缺点:在多个线程同时访问共享数据,可能会出现资源共享问题。
并发执行:在一个时间段内对多个线程依次执行
并行执行:是真正意义上同时执行,两个线程在同一时间节点上一起执行
并发编程的核心问题:
1,不可见性:一个线程对共享变量修改,另一个线程不能立刻看到,称不可见性。(缓存不能及时刷新导致)
public class Demo {
public static void main(String[] args) {
RunTask runTask=new RunTask();//创建任务对象
Thread thread=new Thread(runTask);//创建线程
thread.start();//启动线程
while(true){
if(!runTask.isFlag()) {
System.out.println("main:"+runTask.isFlag());
break;
}
}
}
}
public class RunTask implements Runnable{
private boolean flag=false;
@Override
public void run() {
flag=true;//设置此时flag为true
System.out.println(flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
先启动了一个线程任务,设置的flag为true。但是在main线程取出flag为false。发现读取的数据不一致,说明了线程的不可见性。我们该如何避免这种,在一个线程修改后其他线程可以立刻看见修改后的数据呢?----------添加关键字volatile
public class RunTask implements Runnable{
private volatile boolean flag=false;
@Override
public void run() {
flag=true;//设置此时flag为true
System.out.println(flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
为要修改的变量添加修饰volatile发现可以使改变的变量立即被看见。
volatile修饰的变量在一个线程中被修改后,对其他线程立即可见。并且禁止指令重排。
volatile的底层实现
该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。volatile 的内存语义和synchronized有相似之处,具体来说就是,当线程写入了 volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取 volatile 变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。
volatile 虽然提供了可见性保证,但并不保证操作的原子性。
那么一般在什么时候才使用 volatile关键字呢?
①写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取一计算一写入三步操作,这三步操作不是原子性的,而 volatile 不保证原子性。
②读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为volatile的。
2,乱序性:指令在执行过程中改变顺序,可能会影响程序运行结果
为了优化性能,有时候会改变程序中语句的先后顺序。
public class Test {
static int a = 0, b = 0, x = 0, y = 0;
public static void main(String[] args) {
while (true) {
x=0;
y=0;
a=0;
b=0;
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (x == 0 && y == 0) {
System.err.println(x + "---" + y);
break;
}else{
System.out.println(x+" "+y);
}
}
}
}
每次执行每个线程的count值都不同。由于三个线程是并发执行,所以不确定他们的执行顺序,这就是并发的乱序问题。
解决办法:
1,上锁
public void increment() {
synchronized (obj) {
count++; // 这是一个原子操作,不可中断
System.out.println("Thread " + Thread.currentThread().getId() + " incremented count to " + count);
}
}
2, volatile
3,非原子性:线程切换带来的非原子性问题
A线程执行时,被切换到B线程。
使用Javap-c命令查看汇编代码,如下所示。
public void inc();
Code:
0:aload0
1:dup
2:getfield
5:Iconst 1
6:ladd
7:putfield10:return
由此可见,简单的++value 由2、5、6、7四步组成,其中第2步是获取当前 value的值并放入栈顶,第5步把常量1放入栈顶,第6步把当前栈顶中两个值相加并把结果放入栈顶,第7步则把栈顶的结果赋给 value变量。因此,Java中简单的一句+value 被转换为汇编后就不具有原子性了。
解决方案:
1,使用synchronized关键字
2,CAS机制:不加锁的机制
public class Demo {
private volatile int count = 0;
public static void main(String[] args) {
Demo demo = new Demo();
// 创建并启动三个线程,它们都会访问demo对象的increment()方法
Thread t1 = new Thread(() -> {
demo.increment();
});
Thread t2 = new Thread(() -> {
demo.increment();
});
Thread t3 = new Thread(() -> {
demo.increment();
});
t1.start();
t2.start();
t3.start();
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出最终的count值,由于并发中的乱序问题,这个值可能会与预期不符
System.out.println("Final count: " + demo.count);
}
public void increment() {
System.out.println("Thread " + Thread.currentThread().getId() + " incremented count to " + ++count);
}
}