1.线程安全问题

多个线程同时运行,线程调度由操作系统决定,程序本身无法决定。

如果多个线程同时读写共享变量,就可能出现问题。

假设有AddThread和DecThread,它们分别对同一个共享变量做加和减运算LOOP次,最终结果应该是0。但某些时候比如LOOP为10000时,结果是错误的。

class AddThread extends Thread{
public void run(){
for(int i=0;i<Main.LOOP;i++){
Main.count += 1;
}
}
} class DecThread extends Thread{
public void run(){
for(int i=0;i<Main.LOOP;i++){
Main.count -= 1;
}
}
} public class Main {
final static int LOOP = 10000;
public static int count = 0;
public static void main(String[] args) throws InterruptedException{
Thread t1 = new AddThread();
Thread t2 = new DecThread();
t1.start();
t2.start();
//等待这两个线程执行结束
t1.join();
t2.join();
System.out.println(count);
}
}


## 2.原子操作
* 因此对共享变量进行写入时,必须保证是原子操作
* 原子操作是指不能被中断的一个或一系列操作

当执行 n = n +1时,编译器会把它编译为3条字节码指令,分别是ILOAD, IADD, ISTORE。所以对于这个简单的赋值语句,它并不是一个原子操作,这就可能导致两个线程在执行这条语句的时候,会出现问题。

假设1:n=100,Thread1执行语句n为101,Thread2再执行n为102。

假设2:Thread1刚执行完ILOAD指令,就被操作系统暂停了,然后Thread2调度执行,结果n变成了101,此后Thread1再度被操作系统调度执行,结果也是101。即n+1的指令被2个线程调用了2次,最终只加了1.



所以我们要保证当Thread1执行时,Thread2不能执行,直到Thread1执行完毕,Thread2才能开始执行。这样运行的结果就是正确的。

要实现这个效果,就要对ILOAD之前和ISTORE之后进行加锁和解锁。

3.同步代码块

Java使用synchronized对一个对象进行加锁:

  • 为了保证一系列操作作为原子操作,必须保证一系列操作过程中不被其他线程执行
        synchronized (lock){
n=n+1;
}

当一个线程想要执行synchronized语句块时,必须首先获得指定对象的锁,这个对象就是synchronized括号里的对象,然后线程再执行synchronized语句块,执行结束以后释放锁。

在执行synchronized语句块时,如果Thread1执行到任何语句时,被操作系统中断。其他线程如Thread2因为无法获取lock对象的锁,从而导致Thread2无法进入synchronized语句块,Thread2就必须等待,直到Thread1再次被调用,并执行完synchronized语句块释放了锁,Thread2才能获得lock对象锁,进入synchronized语句块。

synchronized保证了代码块和任意时刻最多只有一个线程能执行。

  • 因为一个对象的锁只能被一个线程获得,其他线程必须等待。

synchronized的问题:

  • 性能下降。因为synchronized代码块无法并发执行,所以性能会下降。此外加锁和解锁都会消耗一定的时间,所以synchronized会降低程序的执行效率。

如何使用synchronized:

  • 1.找出修改共享变量的线程代码块
  • 2.选择一个实例作为锁
  • 3.使用synchronized(lock Object){...}

注意:

  • 对于同一个变量的修改,必须要获取同一个锁,如果2个线程获取的是不同的锁,它们是没有办法进行同步的。
  • 不用担心异常。无论有无异常,在synchronized结束时都会释放锁。
class AddThread extends Thread{
public void run(){
for(int i=0;i<Main.LOOP;i++){
synchronized (Main.LOCK) {
Main.count += 1;
}
}
}
} class DecThread extends Thread{
public void run(){
for(int i=0;i<Main.LOOP;i++){
synchronized (Main.LOCK) {//对于同一个变量的修改,要使用同一个锁
Main.count -= 1;
}//无论有无异常,都会在此释放锁
}
}
} public class Main {
final static int LOOP = 10000;
public static int count = 0;
public static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException{
Thread t1 = new AddThread();
Thread t2 = new DecThread();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}

4.JVM的原子操作

JVM定义了几种原子操作:

  • 基本类型(long和double除外)赋值
  • 引用类型赋值

注意:

  • 原子操作时不需要同步的。
  • 可以把非原子操作变为原子操作
  • 局部变量不需要同步
    //原子操作不需要同步
public void set(int m){
synchronized (obj){
this.value = m;
}
}
//->
public void set(int m){
this.value = m;
}
//对2个int类型进行赋值,它不是一个原子操作。但可以先构造一个int数组,然后利用引用类型赋值,把它变成1个原子操作。
class Pair{
int first;
int last;
public void set(int first,int last){
synchronized (this){
this.first = first;
this.last = last;
}
}
}
//->
class Pair{
int[] pair;
public void set(int first,int last){
int[] ps = new int[]{first,last};
this.pair = ps;
}
}
//a,b,s1,s2,r都是局部变量,各个线程的局部变量是完全独立的,互不影响,所以这个方法不需要同步。
public int avg(int a, int b){
int s1 = a*a + b*b;
int s2 = a + b;
int r = s1/s2;
return r;
}

5.总结:

  • 多线程同时修改变量,会造成逻辑错误

    * 需要通过synchronized同步

    * 同步的本质就是给指定对象加锁

    * 注意加锁对象必须是同一个实例
  • 对JVM定义的单个原子操作不需要同步

廖雪峰Java11多线程编程-2线程同步-1同步代码块的更多相关文章

  1. 廖雪峰Java11多线程编程-2线程同步-3死锁

    1.线程锁可以嵌套 在多线程编程中,要执行synchronized块: 必须首先获得指定对象的锁 Java的线程锁是可重入的锁.对同一个对象,同一个线程,可以多次获取他的锁,即同一把锁可以嵌套.如以下 ...

  2. 廖雪峰Java11多线程编程-2线程同步-2synchronized方法

    1.Java使用synchronized对一个方法进行加锁 class Counter{ int count = 0; public synchronized void add(int n){ cou ...

  3. 廖雪峰Java11多线程编程-2线程同步-4wait和notify

    wait和notify synchronized解决了多线程竞争的问题 我们可以在synchronized块中安全的对一个变量进行修改,但是它没有解决多线程协调的问题. 例如设计一个TaskQueue ...

  4. 廖雪峰Java11多线程编程-1线程的概念-1多线程简介

    多任务 现代操作系统(windows,MacOS,Linux)都可以执行多任务: 多任务就是同时运行多个任务,例如同时开启钉钉.百度网盘.火狐.谷歌.ps等 操作系统执行多任务就是让多个任务交替执行, ...

  5. 廖雪峰Java11多线程编程-4线程工具类-1ThreadLocal

    多线程是Java实现多任务的基础: Thread ExecutorService ScheduledThreadPool Fork/Join Thread对象代表一个线程:调用Tread.curren ...

  6. 廖雪峰Java11多线程编程-1线程的概念-2创建新线程

    Java语言内置多线程支持: 一个Java程序实际上是一个JVM进程 JVM用一个主线程来执行main()方法 在main()方法中又可以启动多个线程 1.创建新线程 1.1 方法一:使用Thread ...

  7. 廖雪峰Java11多线程编程-1线程的概念-5中断线程

    1.中断线程: 如果线程需要执行一个长时间任务,就可能需要中断线程.场景:从网络上下载一个100M的文件,用户在下载过程中中断下载任务的执行. 中断线程就是其他线程给该线程发一个信号,该线程收到信号后 ...

  8. 廖雪峰Java11多线程编程-1线程的概念-3线程的状态

    1线程的状态 线程终止的的原因: run()或call()方法执行完成,线程正常结束 线程抛出一个未捕获的Exception或Error 直接调用该线程的stop()方法来结束该线程--该方法容易导致 ...

  9. 廖雪峰Java11多线程编程-3高级concurrent包-2ReadWriteLock

    ReentrantLock保证单一线程执行 ReentrantLock保证了只有一个线程可以执行临界区代码: 临界区代码:任何时候只有1个线程可以执行的代码块. 临界区指的是一个访问共用资源(例如:共 ...

随机推荐

  1. thinkphp 应用编译

    应用编译机制作为ThinkPHP独创的功能特色,从1.0版本就延续至今,3.2版本的编译机制更加具有特色. 应用编译缓存 编译缓存的基础原理是第一次运行的时候把核心需要加载的文件去掉空白和注释后合并到 ...

  2. 学习servlet之路--javax-servlet不存在

    我在编译一个servlet菜鸟教程下提供的包含有javax.servlet包的java文件,变异出错, import java.io.*;import javax.servlet.*;import j ...

  3. hdu多校第三场 1007 (hdu6609) Find the answer 线段树

    题意: 给定一组数,共n个,第i次把第i个数扔进来,要求你删掉前i-1个数中的一些(不许删掉刚加进来这个数),使得前i个数相加的和小于m.问你对于每个i,最少需要删掉几个数字. 题解: 肯定是优先删大 ...

  4. VS2010-MFC(常用控件:按钮控件的编程实例)

    转自:http://www.jizhuomi.com/software/184.html 因为Button控件在前面的例子中涉及到了,比较简单,本文就不作深入分析了,而是重点讲解单选按钮Radio B ...

  5. Java-Maven-pom.xml-project-build-plugins:plugins

    ylbtech-Java-Maven-pom.xml-project-build-plugins:plugins 1. platform返回顶部 1.maven-compiler-plugin < ...

  6. day22_6-re模块

    # 参考资料:# python模块(转自Yuan先生) - 狂奔__蜗牛 - 博客园# https://www.cnblogs.com/guojintao/articles/9070485.html ...

  7. PHP面向对象编程题(方法的实践)

    <?php header('content-type:text/html;charset=utf-8'); /*设计一个peron类(有名字,年龄和蛋糕三个属性) 蛋糕一共1000块,是所有人共 ...

  8. java中字符数组与字符串之间互相转换的方法

    public static void main(String[] args) { //1.字符数组 转换成 字符串 //(1)直接在构造String时转换 char[] array = new cha ...

  9. 0讲Java,给你一条简单清晰的学习道路!

    1. 计算机基础: 1.1 数据结构基础: 主要学习:1.向量,链表,栈,队列和堆,词典.熟悉2.树,二叉搜索树.熟悉3.图,有向图,无向图,基本概念4.二叉搜索A,B,C类熟练,9大排序熟悉.5.树 ...

  10. ASP.NET WEB API 特性路由

    一.什么是特性路由? 特性路由是指将RouteAttribute或自定义继承自RouteAttribute的特性类标记在控制器或ACTION上,同时指定路由Url字符串,从而实现路由映射,相比之前的通 ...