Java 多线程(一)线程间的互斥和同步通信

35 篇文章 11 订阅

Java 多线程 系列文章目录:


本文主要内容:

  • Thread 线程
  • Thread 线程
  • 传统的定时器
  • 线程之间的互斥和同步通信
  • 线程之间的同步通信
  • 线程范围内共享数据(ThreadLocal)
  • 多个线程访问共享对象和数据的方式

 

Thread 线程

什么是线程,线程就是程序执行的线索,Java是面向对象的语言什么类来表示这样一个东西呢?
Thread 通过 start() 方法来启动,线程所要执行的任务放在 run() 方法里面

下面可以看一下 run() 方法里面的源码:

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

创建线程的两种传统方式:

方式一:

new Thread(){
    @Override
    public void run() {
        // do something...
    }
}.start();


方式二:

new Thread(new Runnable() {
    @Override
    public void run() {
        // do something...
    }
}).start();

(注: Runnable类并不是一个线程,它只是线程一个执行单元)

我们来看下 Thread 的构造函数:

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
                  
    // 省略其他代码...
    
    this.target = target;
    
    // 省略其他代码...
}


从 init() 方法中可以看到,其中有一行代码就是对 target(Runnable类型) 的赋值

因为线程所执行的任务都在run()方法里面,那么在 run()方法里面,target就不为 null,然后就调用了Runnale的run()方法

因为我们重写了 Runnable 的 run() 方法,那么最终执行的就是我们所覆写的 run()方法。

如果我们同时实现了 Thread 的 run() 方法又同时覆盖了 Runnable 的 run() 方法。那么到底会执行哪个的 run()方法呢?

根据 Java 的多态,肯定执行的是 Thread 的 run()方法。


传统的定时器

定时器通过 Timer 这个类来描述,通过 schedule() 方法来调度,定时执行的任务通过 TimerTask 来定义。

下面来实现一个简单的定时器,功能如下:每隔 2 秒执行一次,之后隔 4 秒执行一次,然后又隔2秒,就这样轮循下去

public static void main(String[] args) {  
    new Timer().schedule(new MyTimerTask(), 2000);  
    try {  
        while (true) {  
        System.out.println(new Date().getSeconds());  
        Thread.sleep(1000);  
    }  
} catch (InterruptedException e) {  
    e.printStackTrace();  
        }  
    }  
    }  
class MyTimerTask extends TimerTask {  
    static int count = 0;  
    @Override  
    public void run() {  
        count = (count + 1) % 2;//count=0或1  
        System.out.println("boming");  
        Timer timer = new Timer();  
        timer.schedule(new MyTimerTask(), 2000 + (2000) * count);  
    }  


线程之间的互斥和同步通信


当两个线程去同时操作一个字符串,那么可能会出现线程安全问题。这样的情况可以用银行转帐来解释。

下面的代码就会出现问题:

public class Test {
    public static void main(String[] args) {
        final Outputer outputer = new Outputer();
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    outputer.print("西门吹雪");
                }
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    outputer.print("爱新觉罗");
                }
            }
        }.start();
    }
}

class Outputer {
    public void print(String name) {
        for (int i = 0; i < name.length(); i++) {
            System.out.print(name.charAt(i));
        }
        System.out.println();//打印完字符串换行
    }
}


我们使用两个线程去调用 print(String name) 方法,当第一个方法还没有执行完毕,第二个方法来执行,那么打印出来的 name 就会出现为问题。

代码运行结果如下所示:

爱西门吹雪
新觉罗
爱西门吹新雪
觉罗
爱新觉罗
西门吹雪
西爱门吹新觉罗
雪
西门吹雪
爱新觉罗
爱西门吹雪
新觉罗
爱西门吹雪
...

现在我们要实现的是:只有当第一个线程执行完毕后,第二个线程才能执行 print(String name) 方法,这就必须互斥或者说同步

我们知道实现同步可以使用同步代码块或者同步方法,想到同步(Synchronized)那么自然而然就想到同步监视器,这是两个很重要的概念。

现在我们来改造上面 Outputer 的 print(String name) 方法.

public void print(String name) {
    //synchronized()里面的参数就是同步监视器
    synchronized (this) {
        for (int i = 0; i < name.length(); i++) {
            System.out.print(name.charAt(i));
        }
        System.out.println();//打印完字符串换行
    }
}

如果将 synchronized 关键字放在成员方法上,那么同步锁就是 this 对象:

public synchronized void print(String name) { 
        for (int i = 0; i < name.length(); i++) {
            System.out.print(name.charAt(i));
        }
        System.out.println();//打印完字符串换行 
}

如果在静态方法加上 synchronized 关键字,那么同步锁就是方法所在类的 class 对象:

public static synchronized void print(String name) { 
        for (int i = 0; i < name.length(); i++) {
            System.out.print(name.charAt(i));
        }
        System.out.println();//打印完字符串换行 
}

以上三种方式都可以,程序运行结果:

西门吹雪
爱新觉罗
西门吹雪
爱新觉罗
爱新觉罗
西门吹雪
爱新觉罗
西门吹雪
爱新觉罗
西门吹雪
...


  
线程之间的同步通信


通过一道面试提来解释:子线程循环10次,接着主线程循环100,接着又回到子线程循环10次,接着再回到主线程又循环100,如此循环50次。

public static void main(String[]args){
    new Thread(new Runnable(){
        @Override
        public void run(){
            for(int k=1;k<=50;k++){
                for(int i=1;i<=10;i++){
                    System.out.println("sub thread sequence"+i
                            +"loop of"+k);
                }
            }
        }
    }).start();
    for(int k=1;k<=50;k++){
        for(int i=1;i<=100;i++){
            System.out.println("main thread sequence"+i+"loop of"+k);
        }
    }
}

这样主要的程序逻辑是实现了,但是执行的次序乱来,子线程执行 10 次不应该别打断,主线程执行 100 次也不应该被打断.

所以我们自然就想到了同步,只需要把子循环使用同步代码块,但是用什么作为同步监视器呢?this 显然不行的

当然该类的字节码 class 是可以的,但是这样有2个问题:

  • 第一,虽然实现了同步,但是,不是子线程一次,主线程一次,所以在子/主(线程)次序上还是乱了.
  • 第二,使用 class 作为同步监视器不好,如果程序逻辑很复杂,需要多组需要互斥,使用 class 作为同步监视器,那么就成了一组了。所以这也不好. ( 关于多组互斥可以查看博客 http://blog.csdn.net/johnny901114/article/details/7854666)


经验总结:要用到共同数据(包括同步锁)或共同算法的若干个方法,应该归在同一个类上,这种设计体现了高内聚和程序的健壮性。


比如,下面一个用户登录的逻辑:

可以把 cookie 加密和解密的方法都放到 CookieUtils 类中。

 

我们将上面的代码,两个线程中执行的逻辑抽取到 Business 类中,如:

class Business {

    public synchronized void sub(int k) {
        for (int i = 1; i <= 10; i++) {
            System.out.println("sub thread sequence " + i + " loop of " + k);
        }
    }

    public synchronized void main(int k) {
        for (int i = 1; i <= 100; i++) {
            System.out.println("main thread sequence " + i + " loop of " + k);
        }
    }
}


这样就把相关的方法写到一个类里面了。但是这里还是没有解决通信问题。最终代码如下:

public class Test {
    public static void main(String[] args) {
        final Business business = new Business();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int k = 1; k <= 50; k++) {
                    business.sub(k);
                }
            }
        }).start();

        for (int k = 1; k <= 50; k++) {
            business.main(k);
        }
    }
}


class Business {

    //默认子线程先执行
    boolean isShouldSub = true;

    public synchronized void sub(int k) {
        //此处用 while 最好,因为可能出现假唤醒,//用while的话还会重新判断,这样程序更加严谨和健壮
        if (!isShouldSub) {
            try {
                this.wait();//this表示同步监视器对象
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        for (int i = 1; i <= 10; i++) {
            System.out.println("sub thread sequence " + i + " loop of " + k);
        }
        //子线程做完了,把它置为false
        isShouldSub = false;
        //并且唤醒主线程
        this.notify();
    }

    public synchronized void main(int k) {
        //此处用while最好,因为可能出现假唤醒(API文档里有介绍),//用while的话还会重新判断,这样程序更加严谨和健壮
        if (isShouldSub) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        for (int i = 1; i <= 100; i++) {
            System.out.println("main thread sequence " + i + " loop of " + k);
        }

        //主线程做完了,把它置为true
        isShouldSub = true;
        //并且唤醒子线程
        this.notify();
    }
}


线程范围内共享数据(ThreadLocal)


下面通过一个简单的示例来描述线程之间共享数据:

private static int k = 0;  
public static void main(String[] args) {  
for (int i = 0; i < 2; i++) {  
    new Thread(new Runnable() {  
        @Override  
        public void run() {  
            k = new Random().nextInt();  
            System.out.println(Thread.currentThread().getName()  
                    + " put value to i " + k);  
            new A().get();  
            new B().get();  
        try {  
                Thread.sleep(10);  
            } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }).start();  
    }  
}  
//模块A  
static class A {  
    public void get() {  
        System.out.println("A from " +Thread.currentThread().getName() + " get     value "+ k);  
    }  
}  
//模块B  
static class B {  
    public void get() {  
        System.out.println("A from " +Thread.currentThread().getName() + " get value "+ k);  
    }  
}   



再例如,现在我们需要这样的效果,假设线程 0 给 i 赋值为 1,那么当线程 0 取的时候也是 1,也就是说线程之间取各自放进去的值,而上面的程序达不到这样的要求。

这就需要线程范围内的数据共享。那么我们可以用下面的方式来实现,这也是线程范围内数据共享的原理。

定义一个 Map 集合 key和 value 分别为 Thread 和 Integer。

把给 i 赋值的代码替换为:

int k =new Random().nextInt();
map.put(Thread.currentThread(), k);

get() 方法内的代码改为:

System.out.println("A from " + Thread.currentThread().getName()+ " get value " + map.get(Thread.currentThread()));

这样的话就实现了线程范围内的数据共享了,线程取得值是各自放进去的。

这有什么用呢?比如事务,所谓事务的回滚和提交指的是在一个线程上的,如果是在不同的线程上,那么逻辑就乱了.这不是我们想要的,这样的话我们就可以通过线程范围内共享数据,也就是把连接绑定到该线程上,那么在该线程获取的连接是同一个连接。

 

说了这么多,我们将上面程序改造一些吧。通过 ThreadLocal 来实现:

public class ThreadLocalTest {   
    public static void main(String[] args) {  
        for (int i = 0; i < 2; i++) {  
            new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    int k = new Random().nextInt();  
                    ThreadShareData.getThreadShareData().setAge(k);  
                    ThreadShareData.getThreadShareData().setName("name" + k);  
  
                    System.out.println(Thread.currentThread().getName()  
                            + " put value to i " + k);  
                    new A().get();  
                    new B().get();  
                    try {  
                        Thread.sleep(10);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
            }).start();  
        }  
    }  
  
    // 模块A  
    static class A {  
        public void get() {  
            ThreadShareData data = ThreadShareData.getThreadShareData();  
            System.out.println("A from " + Thread.currentThread().getName()  
                    + " get value " + data.getName() + "--" + data.getAge());  
        }  
    }  
  
    // 模块B  
    static class B {  
        public void get() {  
            ThreadShareData data = ThreadShareData.getThreadShareData();  
            System.out.println("B from " + Thread.currentThread().getName()  
                    + " get value " + data.getName() + "--" + data.getAge());  
        }  
    }  
}  
  
class ThreadShareData {  
    private static ThreadLocal<ThreadShareData> local = new ThreadLocal<ThreadShareData>();  
    private ThreadShareData() {  
    }  
    public static ThreadShareData getThreadShareData() {  
        ThreadShareData data = local.get();  
        if (data == null) {  
            data = new ThreadShareData();  
            local.set(data);  
        }  
        return data;  
    }  
    private String name;  
    private int age;  
    public String getName() {  
        return name;  
    }  
    public void setName(String name) {  
        this.name = name;  
    }  
    public int getAge() {  
        return age;  
    }  
    public void setAge(int age) {  
        this.age = age;  
    }  
}  


上面的例子,对于线程范围内共享对象是一个比较优雅的设计方案,ThreadShareData 有 name 和 age 两个属性,这个类的实例是与每个线程相关的。那么这个设计就交给这个类自己吧,其他用户在任意线程调用我这个类的方法,自然而然就是与线程相关的实例。因为里面我们封装了一个 ThreadLocal 对象.

那么我们是否考虑到如果成千上万的线程来访问,那么是不是可能会导致内存溢出呢?

其实当一个线程死亡,那么系统会把该线程在 ThreadLocal 产生的数据清除掉。

 

多个线程访问共享对象和数据的方式


如果每个线程执行的代码相同,额可以使用相同的 Runnable 对象,这个 Runnable 对象中有那个共享数据。例如,买票系统可以这么来实现:

 

public static void main(String[] args) {   
        MyRunnable myRunnable = new MyRunnable();  
        new Thread(myRunnable).start();  
        new Thread(myRunnable).start();  
        new Thread(myRunnable).start();  
        new Thread(myRunnable).start();  
    }  
    static class MyRunnable implements Runnable {  
        int count = 100;  
        @Override  
        public void run() {  
            synchronized (this) {//同步  
                while (true) {  
                    if (count > 0) {  
                        try {  
                            //模拟线程安全问题,所以要同步/互斥  
                            Thread.sleep(10);  
                        } catch (InterruptedException e) {  
                            e.printStackTrace();  
                        }  
                        count--;  
                    } else {  
                        break;  
                    }  
                    System.out.println(count);  
                }  
            }  
        }  
    }  


如果每个线程执行的代码不同,比如一个线程对一个整型执行加操作,另一个线程对该整型进行减操作。

这时候需要用不同的 Runnable 对象,有如下三种方式来实现这些 Runnable 对象的数据共享。

将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个 Runnable 对象,每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行各个操作的互斥和通信。

 

public static void main(String[] args) {   
       ShareData shareData = new ShareData();  
       new Thread(new MyRunnable(shareData)).start();  
       new Thread(new MyRunnable2(shareData)).start();  
   }  
  
   static class MyRunnable implements Runnable {  
       private ShareData shareData;  
  
       public MyRunnable(ShareData shareData) {  
           this.shareData = shareData;  
       }  
  
       @Override  
       public void run() {  
           shareData.increase();  
       }  
   }  
   static class MyRunnable2 implements Runnable {  
       private ShareData shareData;  
  
       public MyRunnable2(ShareData shareData) {  
           this.shareData = shareData;  
       }  
  
       @Override  
       public void run() {  
           shareData.decrease();  
       }  
   }  
   static class ShareData {  
       int count = 100;  
  
       public void increase() {  
           count++;  
       }  
  
       public void decrease() {  
           count--;  
       }  
   }  

将这些 Runnable 对象作为某一类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行各个操作的互斥和通信,作为内部类的各个 Runnable 对象调用外部类的这些方法.

 

static ShareData shareData = new ShareData();   
    public static void main(String[] args) {  
        //final ShareData shareData = new ShareData();   
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                shareData.decrease();  
            }  
        }).start();  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                shareData.increase();  
            }  
        }).start();  
    }  
    static class ShareData {  
        int count = 100;  
  
        public void increase() {  
            count++;  
        }  
  
        public void decrease() {  
            count--;  
        }  
    }  


上面两种方式的组合:将共享数据封装在另一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或者方法中的局部变量,每个线程的 Runnable 对象作为外部类中的成员内部类或者局部内部类.

public class ThreadTest1 {   
 private int j;   
 public static void main(String args[]){   
    ThreadTest1 tt=new ThreadTest1();   
    Inc inc=tt.new Inc();   
    Dec dec=tt.new Dec();   
    for(int i=0;i<2;i++){   
        Thread t=new Thread(inc);   
        t.start();   
            t=new Thread(dec);   
        t.start();   
        }   
    }   
 private synchronized void inc(){   
    j++;   
    System.out.println(Thread.currentThread().getName()+"-inc:"+j);   
    }   
 private synchronized void dec(){   
    j--;   
    System.out.println(Thread.currentThread().getName()+"-dec:"+j);   
    }   
 class Inc implements Runnable{   
    public void run(){   
        for(int i=0;i<100;i++){   
        inc();   
        }   
    }   
 }   
 class Dec implements Runnable{   
    public void run(){   
        for(int i=0;i<100;i++){   
        dec();   
        }   
    }   
 }   
} 

总之,要同步互斥的几段代码最好分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现他们之间的同步互斥和通信.


 

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Chiclaim

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值