美文网首页Android技术知识Android文章Android开发
Android小知识-Java多线程相关(同步方法与同步代码块)

Android小知识-Java多线程相关(同步方法与同步代码块)

作者: 爱读书的顾先生 | 来源:发表于2018-11-16 22:53 被阅读8次

    本平台的文章更新会有延迟,大家可以关注微信公众号-顾林海,如果大家想获取最新教程,请关注微信公众号,谢谢!

    “非线程安全”是指在多个线程对同一个对象中的实例变量进行并发访问,导致读取到的数据与预期不符,也就是“脏读”,而“线程安全”就是指获得的实例变量的值是经过同步处理的,不会出现“脏读”现象。

    如果是方法内的私有变量就不会存在“非线程安全”问题,也就说“非线程安全”的问题存在于“实例变量”中,我们看下面这段代码:

    public class Task {
    
        private String name = "bill";
    
        public void setName(int index) {
            try {
                Thread.sleep(2000);
                switch (index) {
                    case 1:
                        name = "jack";
                        break;
                    case 2:
                        name = "rose";
                        break;
                    default:
                        name = "default";
                        break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("name="+name);
        }
    }
    

    Task这个类很简单,内部只有一个setName方法,传入一个整型参数,如果是1,name就被赋值为jack,如果是2,name就被赋值为rose,最后打印name。接下来就把这个Task实例交给两个线程去处理。

    public class ThreadFirst extends Thread {
    
        private Task mTask;
    
        public ThreadFirst(Task task) {
            this.mTask = task;
        }
    
        @Override
        public void run() {
            super.run();
            mTask.setName(1);
        }
    }
    
    public class ThreadSecond extends Thread {
    
        private Task mTask;
    
        public ThreadSecond(Task task) {
            this.mTask = task;
        }
    
        @Override
        public void run() {
            super.run();
            mTask.setName(2);
        }
    }
    

    两个线程类都差不多,唯一不同的地方就是调用mTask的setName方法分别传入1和2,如果执行这两个线程,是不是输出两个不同的name值,我们看Client代码:

    public class Client {
    
        public static void main(String[] args) {
            Task task=new Task();
            Thread threadFirst=new ThreadFirst(task);
            Thread threadSecond=new ThreadSecond(task);
            threadFirst.start();
            threadSecond.start();
        }
    
    }
    

    看看打印结果:

    name=jack
    name=jack
    

    发现输出的结果和我们预期不一样,这就是“非线程安全”问题,如何解决呢,可以按照上一节Android小知识-关于多线程的基础知识了解下中提到的给setName方法前加上关键字synchronized,代码如下:

    public class Task {
    
        private String name = "bill";
    
        synchronized public void setName(int index) {
            try {
                Thread.sleep(2000);
                switch (index) {
                    case 1:
                        name = "jack";
                        break;
                    case 2:
                        name = "rose";
                        break;
                    default:
                        name = "default";
                        break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("name=" + name);
        }
    }
    

    打印结果:

    name=jack
    name=rose
    

    添加关键字synchronized后,这个setName方法就是一个同步方法,多个线程执行该方法时是排队执行的。

    现在Task类中只有一个 同步方法,再添加一个非同步方法:

    public class Task {
    
        private String name = "bill";
    
        synchronized public void setName(int index) {
            try {
                Thread.sleep(2000);
                switch (index) {
                    case 1:
                        name = "jack";
                        break;
                    case 2:
                        name = "rose";
                        break;
                    default:
                        name = "default";
                        break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name);
        }
    
        public void getName() {
            System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name);
        }
    }
    

    新添加了一个getName方法,打印的时候连线程名一起打印,方便我们查看,第一个线程代码不变,两个线程类如下:

    public class ThreadFirst extends Thread {
    
        private Task mTask;
    
        public ThreadFirst(Task task) {
            this.mTask = task;
        }
    
        @Override
        public void run() {
            super.run();
            mTask.setName(1);
        }
    }
    
    public class ThreadSecond extends Thread {
    
        private Task mTask;
    
        public ThreadSecond(Task task) {
            this.mTask = task;
        }
    
        @Override
        public void run() {
            super.run();
            mTask.getName();
        }
    }
    

    Client代码如下:

    public class Client {
    
        public static void main(String[] args) {
            Task task=new Task();
            Thread threadFirst=new ThreadFirst(task);
            threadFirst.setName("ThreadFirst");
            Thread threadSecond=new ThreadSecond(task);
            threadSecond.setName("ThreadSecond");
            threadFirst.start();
            threadSecond.start();
        }
    
    }
    

    代码没什么变化,创建一个Task实例,交由两个线程处理。

    打印结果:

    [ThreadSecond]name=bill
    [ThreadFirst]name=jack
    

    按照预期应该是先执行setName方法打印jack,再执行getName方法打印jack,现在是先执行了getName方法,再执行setName方法,也就是说,ThreadFirst线程先持有了object对象的Lock锁,ThreadSecond线程可以以异步的方式调用objec对象中的非synchronized类型的方法。

    现在我们给getName方法前也加上关键字synchronized:

        synchronized public void getName() {
            System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name);
        }
    

    执行程序,打印:

    
    [ThreadFirst]name=jack
    [ThreadSecond]name=jack
    

    这样的话ThreadFirst先持有object对象的Lock锁,ThreadSecond线程如果在这时调用object对象中的synchronized类型的方法则需等待,也就是同步。

    通过多个线程调用同一个方法时,为了避免数据出现交叉的情况,使用synchronized关键字来进行同步,虽然在赋值时进行了同步,但在取值时有可能出现“脏读”,发生“脏读”的情况是在读取实例变量时,此值已经被其它线程更改过了,看下面代码:

    public class Task {
    
        private String name = "bill";
        private String password="12345";
    
        synchronized public void setName(String name,String password) {
            try {
                this.name=name;
                Thread.sleep(5000);
                this.password=password;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name+"  password="+password);
        }
    
        public void getInfo(){
            System.out.println("[" + Thread.currentThread().getName() + "]" + "name=" + name+"  password="+password);
        }
    
    }
    

    setName是一个同步方法,传入姓名和密码,在赋值密码前先休眠5秒,最后打印用户名和密码,而getInfo是非同步方法,用来打印用户名和密码。

    public class ThreadFirst extends Thread {
    
        private Task mTask;
    
        public ThreadFirst(Task task) {
            this.mTask = task;
        }
    
        @Override
        public void run() {
            super.run();
            mTask.setName("jack","poiuytr");
        }
    }
    

    ThreadFirst线程类很简单,就是调用mTask实例的setName方法,设置用户名为jack,密码为poiuytr。

    public class Client {
    
        public static void main(String[] args) {
            Task task=new Task();
            Thread threadFirst=new ThreadFirst(task);
            threadFirst.setName("ThreadFirst");
            threadFirst.start();
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            task.getInfo();
        }
    
    }
    

    创建Task实例和线程实例threadFirst,执行threadFirst线程,休眠2秒后获取相关信息。

    打印:

    [main]name=jack  password=12345
    [ThreadFirst]name=jack  password=poiuytr
    

    通过打印结果就可以看出数据出现了脏读,出现脏读的原因是因为getInfo方法并不是同步的,所以可以在任意时候调用,解决办法就是加上同步synchronized关键字。

    关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁,在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

    用关键字synchronized声明方法在某些情况下是有弊端的,当某个线程调用同步方法执行一个长时间的任务,那么其他线程就必须等待比较长的时间,在这样的情况下可以使用synchronized同步语句块来解决,synchronized方法是对当前对象进行加锁,而synchronized代码块是对某一个对象进行加锁。在使用同步synchronized代码块时需要注意的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中的所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器”是一个。

    多个线程调用同一个对象中的不同名称的synchronized同步方法或synchronized(this)同步代码块时,调用的效果就是按顺序执行的,也就是同步,阻塞的。为此Java提供“任意对象”作为“对象监视器”来实现同步的功能。这个任意对象大多数是实例变量及方法的参数,使用格式为synchronized(非this对象)。


    838794-506ddad529df4cd4.webp.jpg

    相关文章

      网友评论

        本文标题:Android小知识-Java多线程相关(同步方法与同步代码块)

        本文链接:https://www.haomeiwen.com/subject/dvbvfqtx.html