关于java多线程核心技术这本书的总结

这本书,对于入门多线程有一个很大的帮助,但是想要实现真正的高并放,高可用的程序,这些还远远不够.但是你没有多线程基础和思想,想要去实现后面的高并发和高可用也是不现实的.

以下将会对java多线程核心技术这本书进行总结概况:

 首先你要了解多线程那你就得清楚进程和线程到底是什么,当初阿里面试官电面的第一个问题就是解释一下线程是什么?我个人对线程的定义是,进程简单来说就是一个程序的执行,而一个程序的执行里面就一定会有线程,你运行一个java程序的时候你就会发现,你程序肯定要靠main函数来执行,而这个main就是一个线程,

public class TestMain {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		System.out.println("当前线程的名字:"+Thread.currentThread().getName());
	}

}

运行结果:

总结来说:一个进程就是 一个执行的程序,一个进程里包含一个或者多个线程.

接下来就是关于thread类中方法的使用,这里面主要是需要关注sleep()/wait()/yield()方法的使用(关于notify()/join()/interrupt()在线程通信那里在讲解),此三个方法都是使得线程进入到等待状态(WAITING),在这里我们主要是要区别这三个方法:

sleep()方法根据字面意思来说就是抱着锁睡觉,深层次的理解就是执行该方法的线程进入WAITING状态,而且不释放它所持有的锁,这样对于外面排队将进入该同步区里面的其它线程来说就只能干巴巴的在外面一直等着;

wait()方法也是使得执行该方法的线程进入WAITING状态,与sleep()不同的是它会释放掉它所持有的锁,这样对于外面排队将进入该同步去里面的其他线程就可以进来了;

yield()方法我给它的定义是其实本质上也是让执行该方法的线程进入WAITING,一定要记住一点,书里面没有提到,那就是它也不释放锁,而且书里面还特别强调一点就是它不占用CPU,前面的wait()如果没有设置唤醒时间的话,那他就得一直等下去(sleep()是要设置设置时间的),等到其他线程执行notify()来唤醒他(这里在后面的线程通信具体讲解),而yield不同,它执行了yield()之后马上开始抢夺CPU,所以有可能线程刚执行完yield(),马上又抢到了CPU继续执行;

关于线程优先级的使用和守护线程,我就不多做诠释了,基本上都能理解;

接下来讲解一下线程同步的知识,线程同步是多线程中非常重要的一个知识点,书上有一句话说的好:"外修互斥,内修可见";可想而之,互斥就是我们所说的同步,什么叫同步呢?

在多线程中多个线程同时执行同一个对象的同一个语句块或者方法的时候,如果多个线程同时访问的话,就会出现数据的"脏读",也就是说,当A线程和B线程同时进入到一个方法中,里面有一个变量,A线程将变量设置为1,B线程将变量设置成2,如果A线程比B线程快一点将变量设置为1,然后在该方法体中打印该变量的值,A线程打印出来的值就会是B,B线程打印的还是B,这样就出现了数据的"脏读",现在两个线程同时进入,同时执行就称为异步,而我们需要将该方法同步才能解决数据的"脏读".

同步的话,我们要考虑的其实只有两个synchronized和ReentrantLock类(原子性和可见性在后面分析):

synchronized使用于方法上和语句块中,这个想必大家应该都比较清楚它的用法;

ReentrantLock这个类相对来说大家了解的比较少,其基本使用方法如下:

public class Mylock implements Runnable{
	private ReentrantLock lock=new ReentrantLock();
	@Override
	public void run() {
		// TODO Auto-generated method stub
		lock.lock();
			for(int i=0;i<500;i++) {
				System.out.println(Thread.currentThread().getName()+":"+i);
		}
			lock.unlock();
	}

}

我给它的定义是,相当于一个synchronized块,但是比较灵活一点的就是它可以更好的进行线程间的通信. 

同步考虑到了,难得就可以了吗?先前阿里电面我的时候,问了一个问题,即使你使用了synchronized或者Reentrant类,但是大家还是得排队,效率还是很低,在并发量几百万的情况下你怎么进一步优化.这里的话就要考虑到变量的原子性了.

首先我们考虑到之前那个例子,两个线程A和B的目的都只是对变量进行设置,其余代码根本不需要操作,所以我们只需要对变量进行原子同步就行了,这里就是用到一个同步变量AtomicInteger,对变量进行原子同步;

但是这样其实还没有完,你还得考虑它的可见性,因为在jvm虚拟机中,每个线程为了提高自己的运行效率,第一次new出来从公共区域拿取共享数据之后,就放入到之间线程中私有内存堆中,在执行过程中不在去公共区域拿共享数据,这样的话,当其他线程修改了同一个对象的数据之后,存放到公共区域和自身私有内存堆中,其他线程用的时候它们只会去自己的只有内存堆中拿数据,这样又导致了数据的不同步问题.解决办法通过给变量设置volatile使得每个线程每次取该数据的时候都是去公共区域去该共享数据.

这样的话,多线程的使用基本上算是会了,接下来就是多线程之间的通信了,因为对于现在来说各个线程之间还是相互独立的大家各自执行各自的任务,使用共享了一些数据而已.其他的并没有什么联系.

这里就提到了两个比较重要的方法wait()和notify()/notifyAll();

之前提到wait()方法执行之后,如果没有设置等待时间的话就会一直等待下去,知道有其他线程执行了notify()和notifyAll()才进入队列抢夺锁;这里要注意的就是notify()和notifyAll()的区别,notify()方法只是随机唤醒一个正在等待的线程,而notifyAll()唤醒所有等待线程.

通过线程间的通过可以实现生产者和消费者模式:关于多生产和多消费以及一生产和多消费等细节我就不具体阐述了;(如果需要的话可以在下面留言我再加上).

线程通信间还是就是IO流的通信也是一个很重要的东西,现在我们已经了解了线程间信号的传递,接下里就是线程间数据的传输,和IO流差不多字节流和字符流两种:

1) PipedInputStream 和PipedOutputStream

2)PipedRead 和PipedWrite

代码演示PipedInputStream 和PipedOutputStream:

ReadData类:

package cn.mxl.MxlThread;

import java.io.IOException;
import java.io.PipedInputStream;

public class ReadData {
	public void readData(PipedInputStream pis) {
		try {
			System.out.println("read:");
			byte[] b=new byte[1024];
			if(pis.read(b)!=-1) {
				String data=new String(b);
				System.out.print(data);
			}
			System.out.println();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally {
			try {
				pis.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

WriteData类:

package cn.mxl.MxlThread;

import java.io.IOException;
import java.io.PipedOutputStream;

public class WriteData {
	public void writeMethod(PipedOutputStream pos) {
		try {
			System.out.println("write:");
			for(int i=0;i<10;i++) {
				System.out.print(i+",");
				String data=i+",";
				pos.write(data.getBytes());
			}
			System.out.println();
		} catch (Exception e) {
			// TODO: handle exception
		}finally {
			try {
				pos.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

ThreadRead类和ThreadWrite类:

Run类:

package cn.mxl.MxlThread;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class Run {

	public static void main(String[] args) throws IOException {
		// TODO Auto-generated method stub
		WriteData writeData=new WriteData();
		ReadData readData=new ReadData();
		
		PipedInputStream pis=new PipedInputStream();
		PipedOutputStream pos=new PipedOutputStream();
		
		pos.connect(pis);
		
		ThreadWrite threadWrite=new ThreadWrite(writeData, pos);
		ThreadRead threadRead=new ThreadRead(readData, pis);
		
		Thread threadW=new Thread(threadWrite);
		Thread threadR=new Thread(threadRead);
		
		threadW.start();
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		threadR.start();
		
	}

}

 运行结果:

后面的字符流我就不做累述了,差不多的; 

对于通信还有一个最为重要的使用那就是让线程停止,很久以前传统的方式是使用stop()来停止线程,但是由于它的不稳定性,容易出现一些意料之外的情况,所以已经不建议使用了;现在使用interrupt()来打断线程,其原理就是,当一个线程在执行sleep()/wait()/yield()/join()等方法时候,通过抛出异常来停止该线程.

现在讨论完了普通的线程间的通信之后,我们讨论一下Lock(ReentrantLock)这个类的线程通信;

ReentrantLock类的通信主要依赖于它里面的实例Condition,线程对象可以注册在指定的Condition中,从而可以有选择性地进行线程通信,在调度线程上更加灵活.

MyService类:

package cn.mxl.reentrant;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyService {
	private Lock lock=new ReentrantLock();
	private Condition condition=lock.newCondition();
	public void await() {
		try {
			lock.lock();
			System.out.println("await时间:"+System.currentTimeMillis());
			condition.await();
			lock.unlock();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	public void sinal() {
		lock.lock();
		System.out.println("signal时间:"+System.currentTimeMillis());
		condition.signal();
		lock.unlock();
	}
}

ThreadA类:

package cn.mxl.reentrant;

public class ThreadA implements Runnable{
	private MyService myService;
	public ThreadA(MyService myService) {
		this.myService=myService;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		myService.await();
	}
	
}

Run类:

package cn.mxl.reentrant;

public class Run {

	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		MyService myService=new MyService();
		
		ThreadA threadA=new ThreadA(myService);
		
		Thread thread=new Thread(threadA);
		
		thread.start();
		Thread.sleep(1000);
		myService.sinal();
	}

}

运行结果:

其实相比于普通的synchronized来说,Reentrant类将await(),封装到了类之中(后面可以研究一下Reentrant是怎么实现同步的,以及如何封装这些方法的);

对于Reentrant类中生成者和消费者之间的通信差不多我就不在累述了;

在这里要讲一下公平锁和非公平锁:

公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,而非公平锁就是一种获取锁的抢占机制,是随机的;

公平锁中提到按照线程加锁的顺序分配,其实简单来说就是谁先进入run方法谁就先持有锁,代码如下:

Service类:

package cn.mxl.reentrant.fair;

import java.util.concurrent.locks.ReentrantLock;

public class Service {
	private ReentrantLock lock;
	public Service(boolean isFair) {
		lock=new ReentrantLock(isFair);
	}
	public void serviceMethod() {
		lock.lock();
		System.out.println("ThreadName="+Thread.currentThread().getName()+"获得锁定");
		lock.unlock();
	}
}

ThreadA类:

package cn.mxl.reentrant.fair;

public class ThreadA implements Runnable{
	private Service service;
	public ThreadA(Service service) {
		this.service=service;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		System.out.println(Thread.currentThread().getName()+"运行了!");
		service.serviceMethod();
	}

}

 Run类:

package cn.mxl.reentrant.fair;

public class Run {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Service service=new Service(true);
		
		ThreadA threadA=new ThreadA(service);
		
		Thread[] thread=new Thread[5];
		for(int i=0;i<5;i++) {
			thread[i]=new Thread(threadA);
		}
		for(int i=0;i<5;i++) {
			thread[i].start();
		}
	}

}

运行结果:

注意,观察"获得锁定"的顺序,它与 "运行了"的顺序是一致的.

非公平锁,其实主要修改Reentrant的传入值,设置为false(默认是false),然后就是非公平锁了

Run类的修改:

package cn.mxl.reentrant.fair;

public class Run {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Service service=new Service(false);
		
		ThreadA threadA=new ThreadA(service);
		
		Thread[] thread=new Thread[5];
		for(int i=0;i<5;i++) {
			thread[i]=new Thread(threadA);
		}
		for(int i=0;i<5;i++) {
			thread[i].start();
		}
	}

}

运行结果:

你在仔细观察就会发现,锁定的顺序并不是线程加锁的顺序(就是"运行了"的顺序)来执行的了 .

对于Reentrant的字符流和字节流操作,和之前synchronized一样,使用PipedInputsream和PipedOutputsream;

现在讲解一下关于Reentrant的优化问题,可想而知,Reentrant类具有完全互斥排他的效果,即同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务.其实本质相当于synchronized块,保证了其安全性,但是效率极低,所以ReentrantLock类又提供了一个类ReentrantReadWriteLock类做为读写操作;

我个人感觉它没啥问题,在使用lock.readLock.lock()来进行读锁定,大家可以异步访问,但是你直接不加锁就可以了,因为你只是去读数据,没有什么意义,因为你这个读锁操作还是异步的,虽然读锁和写锁是一起使用了,写锁具有排他性,两个一起使用使得两个都具有了排他性,这对于性能上又有什么作用呢!还不如在写操作这里对于变量直接保证其原子性,使用atomic就可以解决,或者单独对写操作部分代码进行同步块处理就可以了.

书里面谈到的定时器,我就不多做累述,喜欢的自己去看看其他资料;

下面讲解一下单例模式下需要使用到多线程的问题.单例模式本身对于程序来说无处不在,但是单例模式在多线程下也是存在安全问题的;

创建单例模式有两种方式,一种是饿汉模式,一种是懒汉模式;

在饿汉模式中,在new一个对象的时候就已经创建了一个单例,在多线程获取该单例的时候,不会出现多个单例的存在;

而懒汉模式就不同了,他是延迟加载,在调用方法的时候,new单例,这样的话,在多线程中就会出现多个单例的问题;

对于懒汉式的缺点,可以在getIntance()方法上加上同步;

对于后面的线程组感觉没有太多讲解的意义,本身就是一个对线程管理的工具;

总结:这本书太过于理论基础,没有太多的关于这些知识的实际使用的讲解(比如说公平锁和非公平锁使用场景,生产者和消费者模式的使用场景等),使得读者看完之后,很多重要的知识点不知道该如何使用它们,不清楚它们到底该用在哪比较,好.

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值