交替循环,本质思想就是对方法体进行加锁,两个线程相互通信来获取锁、释放锁,达到1212的效果。
【1】extends Thread
实践目标:使用两个线程打印 1-100. 线程1, 线程2 交替打印
其实就是多线程之间的线程通信,使用wait、notify或者notifyAll。如下的三个关键字使用的话,都得在同步代码块或同步方法中。
-
① wait():一旦一个线程执行到wait(),就释放当前的锁。
-
② notify()唤醒wait的一个的线程;
-
③ notifyAll():唤醒所有线程;
示例代码
class PrintNum extends Thread {
static int num = 1;
// 静态成员变量,保证锁的唯一
static Object obj = new Object();
public void run() {
while (true) {
//obj can instead of PrintNum.class
synchronized (obj) {
/*当前线程活动期间才能唤醒其他等待线程*/
obj.notify();
if (num <= 100) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":"
+ num);
num++;
} else {
break;
}
try {
obj.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
public class TestCommunication {
public static void main(String[] args) {
Thread t1 = new PrintNum();
Thread t2 = new PrintNum();
t1.setName("thread1");
t2.setName("thread2");
t1.setPriority(Thread.MAX_PRIORITY);//10
t2.setPriority(Thread.MIN_PRIORITY);//1
t1.start();
t2.start();
}
}
调用start()方法,它 会新建一个线程然后执行run()方法中的代码。但是并不意味着多次调用t1.start()
会创建多个线程并执行run()方法。Thread执行了start之后不可以再次执行start。
那么为什么不直接调用run()方法呢?
如果直接调用run()方法,并不会创建新线程,方法中的代码会在当前调用者的线程中执行。
另外查看Thread会发现,Thread实现了Runnable接口,实现了run方法并自定义了一系列方法。
【2】implements Runnable
package com.web.test;
public class TestThread implements Runnable {
int i = 1;
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
/*指代的为 t,因为使用的是implements方式。若使用继承Thread类的方式,慎用this*/
synchronized (this) {
/*唤醒另外一个线程,注意是this的方法,而不是Thread*/
notify();
try {
/*使其休眠100毫秒,放大线程差异*/
//Thread,currentThread().sleep(0,100);使其休眠100纳秒
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (i<=100) {
System.out.println(Thread.currentThread().getName() + ":"+ i);
i++;
try {
/*放弃资源,等待*/
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) {
/*只有一个TestThread对象*/
TestThread t = new TestThread();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
输出结果如下:
使用lambda实现Runnable
如下所示一个多线程爬虫实例:
Thread thread = new Thread(() -> {
int currTotal=0;
for (int j = beginPage; j <= finalEndPage; j++) {
String sendUrl = note_list_url + j;
Integer pageData = new CrawBookService().getPageData(sendUrl, new AtomicInteger(0));
totalNum.getAndAdd(pageData);
currTotal+=pageData;
}
logger.info("当前线程:{},抓取数量为:{}", Thread.currentThread().getName(),currTotal);
});
【3】释义
① 锁的唯一性
这里使用的锁如下:
static Object obj = new Object();
在Main方法里面我们创建了两个PrintNum对象,那么是不是有两个Object obj = new Object();
?这样岂不是不能保证线程安全,同步失败?
这里就要掌握类的加载及类中成员变量的加载。
② 实例化对象的执行顺序
如果类还没有被加载:
1、先执行父类的静态代码块和静态变量初始化,并且静态代码块和静态变量的执行顺序只跟代码中出现的顺序有关。
2、执行子类的静态代码块和静态变量初始化。
3、执行父类的实例变量初始化(例如:int a;初始化就是0,引用类型就是null)
4、执行父类的构造函数
5、执行子类的实例变量初始化(例如:int a;初始化就是0,引用类型就是null)
6、执行子类的构造函数 。
如果类已经被加载:
则静态代码块和静态变量就不用重复执行,再创建类对象时,只执行与实例相关的变量初始化和构造方法。
即 虽然创建了两个PrintNum对象,但是静态成员变量obj只有一个!这就保证了同步控制。
基于锁的唯一性,那么我们还可以使用PrintNum.class
来代理obj作为锁进行同步控制。