查看Thread类的源码,可以发现它实现了Runnable接口,然后在自己的run方法中调用了Runnable的run方法。这里其实就是静态代理这一设计模式,Thread是代理角色,而Runnable则是真实角色。在Thread的start方法中,会为我们开辟新的线程并执行其run方法,而Thread的run方法会调用它内部持有引用的Runnable的run方法。因此,我们有以下几种方法开辟新线程:
通过重写Thread的run方法,不再传给它Runnable,通过其Runnable的run方法执行,而是在的run方法中直接执行相应的操作。
Thread thread = new Thread(){
@Override
public void run() {
//TODO
}
};
thread.start();
通过创建一个Runnable,通过静态代理的设计,将Runnable对象传给Thread,再通过Thread开辟新线程并做相应操作
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//TODO
}
});
thread.start();
通过Callable
接口实现多线程,比较繁琐,优点是有返回值。
创建一个类实现Callable
接口,其中泛型为返回的值的类型、然后实现Callable的call方法。
class Race implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return 1000;
}
}
借助执行调度工具ExecutorService,创建对应类型线程池。获取Future对象(submit方法中的参数是具体实现类对象)。然后通过Future的get方法即可获取到返回值(此处要做异常处理,将异常throws了)。
get这里会等待线程中步骤执行完然后获取数据。
ExecutorService service = Executors.newFixedThreadPool(2);
Race race = new Race();
Future<Integer> result = service.submit(race);
int num = result.get();
调用ExecutorService的shutdown方法停止服务
service.shutdown();
线程有五大状态,分别是
上面的图是线程的状态的示意图。
当我们创建线程后,它便进入了new(新建)状态。
在我们调用了它的start方法后,它便进入了Runnable(就绪)状态。
经过CPU的调度,它便会进入Running(运行)状态。
如果在运行后执行完毕,这个线程便会进入Dead(死亡)状态。
如果在运行过程中一些事情导致了线程的阻塞,它就会进入Blocked(阻塞)状态。阻塞解除后它便会重新进入Runnable(就绪状态)
当线程体自然执行完毕后,线程便会自然终止。
外部干涉非常简单,比如下面这种方法
线程类中定义线程体使用标志
class Study implements Runnable{
private boolean flag = true;
@Override
public void run() {
//TODO
}
}
线程体内使用flag
class Study implements Runnable{
private boolean flag = true;
@Override
public void run() {
while (flag){
System.out.println("study thread");
}
}
}
提供对外方法改变该标识
class Study implements Runnable{
private boolean flag = true;
...
public void stop(){
this.flag = false;
}
}
外部调用
Study study = new Study();
new Thread(study).start();
for (int i=0;i<100;i++){
if (i==50)
study.stop();
}
这个方法已经被弃用,并且臭名昭著。从他的字面意思上我们可以知道他貌似可以停止一个线程。但实际上它并非是真正意义上的停止线程,而且停止线程还会引来一些其他的麻烦事。
调用Thread.stop()方法是不安全的,这是因为当调用Thread.stop()方法时,会发生下面两件事:
当线程抛出ThreadDeath异常时,会导致该线程的run()方法突然返回来达到停止该线程的目的。ThreadDetath异常可以在该线程run()方法的任意一个执行点抛出。
但是,线程的stop()方法一经调用线程的run()方法就会即刻返回吗?
我们用下面的例子来测试一下
public class ThreadStopTest {
public static void main(String[] args) {
try {
Thread t = new Thread() {
//对于方法进行了同步操作,锁对象就是线程本身
public synchronized void run() {
try {
long start=System.currentTimeMillis();
//开始计数
for (int i = 0; i < 100000; i++)
System.out.println("runing.." + i);
System.out.println((System.currentTimeMillis()-start)+"ms");
} catch (Throwable ex) {
System.out.println("Caught in run: " + ex);
ex.printStackTrace();
}
}
};
//开始计数
t.start();
//主线程休眠100ms
Thread.sleep(100);
//停止线程的运行
t.stop();
} catch (Throwable t) {
System.out.println("Caught in main: " + t);
t.printStackTrace();
}
}
}
会发现,程序运行的结果如图:
…
running…99994
running…99995
running…99996
running…99997
running…99998
running…99999
可以看到,调用了stop方法之后,线程并没有停止,而是将run方法执行完 。这就十分诡异了。根据SUN的文档,原则上只要一调用thread.stop()方法,那么线程就会立即停止,并抛出ThreadDeath异常。
查看Thread的源代码后发现 ,Thread.stop(Throwable obj)方法是同步的 ,而我们工作线程的run()方法也是同步。会导致主线程和工作线程共同争用同一个锁(工作线程对象本身) 。 由于工作线程在启动后就先获得了锁,所以无论如何,当主线程在调用stop()时,它必须要等到工作线程的run()方法执行结束后才能进行,结果导致了上述奇怪的现象。
因此,不要用stop方法停止线程
join可以理解为合并的意思,将多条线程合并为一条。下面是join的几个重载
我们可以通过下面的例子来看看join的效果
class Thread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100 ; i++) {
System.out.println("join..."+i);
}
}
public static void main(String[] args){
Thread1 thread1 = new Thread1();
Thread t = new Thread(thread1); //新生
t.start(); //就绪
//cpu调度后则进入运行状态
for (int i = 0; i < 100 ; i++) {
if (i==50){
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main..."+i);
}
}
}
查看运行结果:
join...0
main...0
join...1
main...1
join...2
main...2
...
main...48
join...49
main...49
join...50
join...51
join...52
...
join...97
join...98
join...99
main...50
main...51
main...52
...
查看运行结果,会发现。在执行join之前,main线程与 t 线程是多线程同时执行的。而当执行了t的join方法之后,等到 t 线程执行完后,main线程才继续执行。也就是在 t 线程执行的过程中,main线程被阻塞了。等到 t 线程执行完,main线程才继续执行。
yield方法可以实现暂停当前线程,执行其他的线程。
它是一个static方法,暂停的具体线程取决于它执行的地方。
sleep方法可以让当前正在执行的线程进行休眠一定秒数。
每一个对象都有一把锁。当该线程休眠的时候,不会释放锁。
sleep可以有以下的作用:
1. 与时间相关:倒计时
2. 模拟网络延时
我们可以为线程通过setPriority方法设置优先级,优先级的范围为1-10。下面是几个优先级的常量:
注意,优先级并不能代表先后顺序,只能导致执行的概率不同。优先级大的线程执行的概率会更大。
同步也称为并发。由于现在有多个线程,就可能导致多个线程访问同一份资源的问题。我们要确保这个资源安全,因此我们对其进行同步的处理。这样就可以保证它线程安全。
也就是说,线程安全的资源就是指多个线程同时访问这个资源的时候,是同步的。这份资源是安全的。
**原子是世界上的最小单位,具有不可分割性。**比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作是原子操作。 比如:a++; 这个操作实际是a = a + 1;是可分割的 ,因此它不是原子操作。
非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。
**在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。 **
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。 也就是一个线程修改的结果。另一个线程马上就能看到。
用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。
需要注意的是,**volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。 **
**在 Java 中 volatile、synchronized 和 final 实现可见性。 **
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
Java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。
JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
介绍
Java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
意义
JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
我们通过synchronized标识符可以保证某一部分的线程安全,它是通过等待来实现的。我们把使用synchronized的行为叫做‘加锁’。当几个线程同时访问一份资源的时候,先到达的线程拿到锁,然后再轮到其他线程来。这样能保证线程的安全。
当我们加上synchronized后,就是为它加了一把锁。当多个线程同时来访问这份资源时,先到达的资源便可以得到这个锁,其他资源只能等待,等待结束后再执行。这样就保证了我们的线程安全。
由于线程安全是通过等待来进行,因此会造成效率的低下。为了减少这种效率损耗,我们应该尽量缩小加锁的范围,来提高效率。
我们都知道,{}及里面的语句表示一个同步块。在它前面加上**synchronized(引用类型)**标识符,就变成了一个同步块。
而synchronized锁 类.class 时,比较特殊。主要是用于给一些静态的对象加锁。(如线程安全的单例模式就会用到)
synchronized(引用类型|this|类.class){
//TODO
}
在方法的前面加上synchronized,就称这个方法是同步方法。0
线程对于普通的共享对象的操作发生在本地内存中,有时可能来不及同步到主内存中,就开始了下一个线程的处理。因此我们就可以用volatile来保证这个共享对象的可见性,强制将内存刷新到主内存中。
volatile具备两种特性,第一就是保证共享变量对所有线程的可见性。将一个共享变量声明为volatile后,会有以下效应:
1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;
2.这个写会操作会导致其他线程中的缓存无效。
过多的同步容易造成死锁。
对一个对象多线程同步访问,容易造成死锁。我们可以通过信号量来解决这个问题。
之前讲到线程锁,一个线程如果锁定了一资源,那么其它线程只能等待资源的释放。也就是一次只有一个线程执行,直到这个线程执行完毕或者unlock。而**信号量(Semaphore)**可以控制多个线程同时对某个资源的访问。
**信号量(Semaphore)**是用来保护一个或者多个共享资源的访问,Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。
就好比一个厕所管理员,站在门口。如果有厕所有空位,就开门允许与空厕数量等量的人进入厕所。多个人进入厕所后,相当于N个人来分配使用N个空位。为了避免多个人来同时竞争同一个厕所,在内部仍然使用锁来控制资源的同步访问。
Semaphore使用时需要先构建一个参数来指定共享资源的数量,Semaphore构造完成后即可获取Semaphore。共享资源使用完毕后释放Semaphore。
Semaphore semaphore = new Semaphore(10,true);
semaphore.acquire();
//TODO
semaphore.release();
比如下面的代码就是模拟控制一个商场厕所的并发使用:
public class ResourceManage {
private final Semaphore semaphore ;
private boolean resourceArray[];
private final ReentrantLock lock;
public ResourceManage() {
this.resourceArray = new boolean[10];//存放厕所状态
this.semaphore = new Semaphore(10,true);//控制10个共享资源的使用,使用先进先出的公平模式进行共享(公平模式的信号量,先来的先获得信号量)
this.lock = new ReentrantLock(true);//公平模式的锁,先来的先选
for(int i=0;i<10; i++){
resourceArray[i] = true;//初始化为资源可用的情况
}
}
public void useResource(int userId){
semaphore.acquire();
try{
int id = getResourceId();//占到一个坑
System.out.print("userId:"+userId+"正在使用资源,资源id:"+id+"\n");
Thread.sleep(100);//do something,相当于于使用资源
resourceArray[id] = true;//退出这个坑
}catch (InterruptedException e){
e.printStackTrace();
}finally {
semaphore.release();//释放信号量,计数器加1
}
}
private int getResourceId(){
int id = -1;
lock.lock();
try {
//虽然使用了锁控制同步,但由于只是简单的一个数组遍历,效率还是很高的,所以基本不影响性能。
for(int i=0; i<10; i++){
if(resourceArray[i]){
resourceArray[i] = false;
id = i;
break;
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
return id;
}
}
public class ResourceUser implements Runnable{
private ResourceManage resourceManage;
private int userId;
public ResourceUser(ResourceManage resourceManage, int userId) {
this.resourceManage = resourceManage;
this.userId = userId;
}
public void run(){
System.out.print("userId:"+userId+"准备使用资源...\n");
resourceManage.useResource(userId);
System.out.print("userId:"+userId+"使用资源完毕...\n");
}
public static void main(String[] args){
ResourceManage resourceManage = new ResourceManage();
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(new ResourceUser(resourceManage,i));//创建多个资源使用者
threads[i] = thread;
}
for(int i = 0; i < 100; i++){
Thread thread = threads[i];
try {
thread.start();//启动线程
}catch (Exception e){
e.printStackTrace();
}
}
}
广告时间
我是N0tExpectErr0r,一名广东工业大学的大二学生
欢迎来到我的个人博客,所有文章均在个人博客中同步更新哦
http://blog.N0tExpectErr0r.cn