Java 多线程 系列文章目录:
- Java 多线程(一)线程间的互斥和同步通信
- Java 多线程(二)同步线程分组问题
- Java 多线程(三)线程池入门 Callable 和 Future
- Java 多线程(四)ThreadPoolExecutor 线程池各参数的意义
- Java 多线程(五)Lock 和 Condition 实现线程同步通信
- Java 多线程(六)Semaphore 实现信号灯
- Java 多线程(七)CyclicBarrier 同步的工具类
- Java 多线程(八)CountDownLatch 同步工具类
- Java 多线程(九)Exchanger 同步工具类
- Java 多线程(十)ArrayBlockingQueue 阻塞队列
- Java 多线程(十一)JDK 同步集合
本文主要内容:
- 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();
}
}
}
}
总之,要同步互斥的几段代码最好分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现他们之间的同步互斥和通信.