一:基本概念:
- 1:什么叫进程以及线程
进程:是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。进程就是我们电脑上运行的一个程序例如:QQ;
线程:就是进程的一个分支,一个进程可以有多个线程,线程是不能拥有资源的,只有一些运行时必要的数据结构;每一个线程都会拥有一个父进程,进程拥有电脑或者程序分配的一些资源,这些资源可以被进程下面的线程所共享;
通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度。
多线程程序是一种多任务多并发的程序;具有以下优点: - A:提高应用程序的相应:例如:当一个程序耗时很长时,如果使用单线程,此时电脑将无法为其他事件作出响应;而多线程则可以为这个耗时很长的程序单独开辟一个线程让其自己去完成,为其他操作开辟另外一个线程,即使响应相关的操作;
- B:使CPU系统更加的有效。当一个电脑上运行有多个CPU时,操作系统就会保证当线程数不大于CPU数目时;不同的线程运行在不同的CPU上;
- C:改善程序的结构;将一个复杂的进程分解为多个线程;使每一个线程成为独立或者半独立的状态;有利于程序的维护或者修改
2:多进程和多线程
多进程是指一个电脑同时运行多个任务的状态
多线程是指同一个程序(任务)中多个顺序流在执行;
二:Java中的多线程:
-
1:实现方式:
- 1:继承Thread类
- 2:实现Runnable接口
- 3:通过Callable 和 Future 创建线程
1:集成Thread类
public class test{
public static void main(String[] arg){
thread1 t1 = new thread1();
t1.start();
}
}
class thread1 extends Thread
public void run(){
System.out.println("我是继承了Thread类实现的多线程!");
}
}
B: 实现了Runnable接口的多线程
public class test{
public static void main(String[] arg){
runnable r = new runnable();
thread1 t1 = new thread1(r);
t1.start();
}
}
class runnable1 implements Runnable{
public void run() {
System.out.println("我是实现了Runable接口的多线程!");
}
}
C:通过实现Callable接口并结构FutureTask创建线程,他和继承Thread线程之间不会共享数据;
class callableThread implements Callable<Integer>{
private int temp = 10;
@Override
public Integer call() throws Exception {
for(int i=0;i<10;i++){
temp--;
System.out.println(Thread.currentThread().getName()+"="+i);
}
return temp;
}
}
public class test {
public static void main(String[] args) {
callableThread ct = new callableThread();
FutureTask<Integer> ft = new FutureTask<>(ct);
FutureTask<Integer> ft1 = new FutureTask<>(ct);
new Thread(ft,"线程1").start();
new Thread(ft1,"线程2").start();
}
}
两种方式的优缺点:
实现Runnable接口可以避免java的单继承的缺点;
实现Runnable接口多线程操作下可以保证线程安全;即实现Runable接口的对象可以被多个线程共享;进而实现变量的共享;适合于多个线程处理同一资源的情况;
三:中断线程:
每一个线程都会有一个标志位用来判断该线程是否被终止;所以每次调用之前我们都应该先检查该线程该标志位判断该线程是否终止;
当线程中的run方法执行到最后一句并经由rerturn语句返回时,或者程序中存在没有捕获的异常时线程将终止;
调用interrup方法可以用来终止线程将其标志置位;
isInterrupt方法也可以用来判断线程是否终止,并且它也不会改变线程的状态;另外一个Interrupted方法也具有该功能,但是它会改变该线程的状态;
四:线程的状态以及各个状态之间的转换:
- A:注意yield方法只允许比自己优先级高的线程或者和自己的优先级相同的线程调用;
- B:join方法是一个线程的方法,例如:一个程序启动了A和B两个线程,其中A是主线程,B是子线程;现在我们正在执行A线程,如果此时A线程需要用到B线程执行完毕时的一个数据,此时我们就可以调用B线程的jion方法让线程B执行完毕之后再执行线程A;可以理解为插队;
五:守护线程:
守护线程的主要作用就是为其他线程服务;例如:计时器,当只剩下守护线程时虚拟机就会关闭自动退出;守护线程不会去访问固定资源:文件,数据库;
通过调用t.setDaemon(true)将线程t变成守护线程
六:同步:
-
<1>:基本概念
当使用多个线程处理同一组数据时,就有可能出现两个线程同时对这一组数据修改的情况;这样就会造成这一组共享的数据出现不合理的情况;为了防止这一情况发生我们提出了同步的概念; - <2>:保持数据同步的方法
- ** A:使用锁对象**
java.util.concurrent下面的ReentrantLock来保护代码的基本结构;
private Lock banklock = new ReentrantLock();
public void transfer(){
MyLock.lock();
Try{
//doSomething();
}Finally{
myLock.unlock();
}
}
上面的结构确保任何时刻仅仅只能有一个线程进入临界区;一旦一个线程封锁了锁对象,其他任何线程都无法通过Lock语句;当其他线程调用lock时他们将被阻塞;直到第一个线程释放锁对象;PS:释放锁的操作一定要放在finally字句中;临界区代码抛出异常,锁对象必须被释放;
锁是可以重入的;线程可以重复的获得已经持有的锁;
-
B:条件对象:
只有当某个条件满足了我们才执行接下来的操作;此时我们就需要添加一个条件
private Condition sufficientFunds;
public void transfer(int from,int to,int amount){
banklock.lock();
try{
//不满足条件就一边等着去
while(accounts[from]<amount){
sufficientFunds.await();
}
//Transfer fund;
....
suffcientFunds.signAll();
}finally{
banklock.unlock();
}
}
调用signAll方法并不会立即激活一个线程,他仅仅是让你从一个对象的等待池中跳出来了;以便这个线程在当前线程释放同步锁之后;可以通过竞争实现对对象的访问;
总结:
a:锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码
b:锁可以管理试图进入被保护代码段的线程
c:锁可以拥有一个或者多个条件对象
d:每一个条件对象管理那些已经进入被保护的代码段但还是不能运行的线程。C:synchronized关键字
java的每一个对象都会有一个内部锁,如过一段代码或者一个方法用synchronized关键字声明,那么对象的锁将会保护这个方法;也就是说想要调用该方法,就必须要获得内部的对象锁;
其实
public synchronized void method(){
//dosomething;
}
等价于:
public void method(){
this.intrinsicLock.lock();
try{
//dosomething;
}
}
内部对象锁仅仅只有一个相关条件。wait方法添加一个线程到等待集中去,notify/notifyAll方法解除等待线程的阻塞状态;等价于Condition的notify和wait以及notifyAll方法;但是notify和notifyAll以及wait方法都是Object的final方法;由上面的代码可知synchronized相比ReenrantLock锁要简单的多;使用对象锁最关键的一点是你要理解 每一个对象都有一个内部锁;并且该锁只有一个内部条件;由锁来管理那些试图进入sychronized方法的线程,由条件来管理那些调用wait方法的线程;
内部锁和条件存在的一些局限性:
a:不能中断一个正在试图获得锁的线程
b:试图获得锁时不能设定超时
c:每个锁仅有单一的条件,可能是不够的;
那么在代码中使用哪一种?
·1)最好既不使用Lock/Condition也不使用synchronized关键字。许多情况我们都可以使用java.util.concurrent包中的一种机制,它会为你处理所有的代码加锁;
2)如果使用synchronized关键字适合你的程序,最好使用它;减少代码量;
3)如果特别需要Lock/Condition结构提供的都具有特性时才使用Lock/Condition;
-
D:同步阻塞
所谓的同步阻塞其实就是使用同步代码代码块;
例如:
private double[] accounts;
public void transfer(Vector<Double> accouts,int to,int from){
synchronized(accounts){//截获这个锁
acounts.set(from,accounts.get(from)-amount);
acounts.set(to,accounts.get(to)+amount);
}
}
-
E:监视器的概念:
虽然锁和条件是十分强大的同步工具,但是他们不是面对对象的;所以人们提出了监视器的概念,它具有以下特征:
a:监视器是只包含私有域的类
b:每一个监视器类的对象仅仅含有一个相关的锁;
c:使用该锁对所有的方法进行加锁;如果客户端调用obj.method();那么obj对象的锁是在方法调用开始时自动获得的;并且当方法返回时自动释放该锁;
d:该锁可以有任意多个相关条件
但是Java设计者以不是很精确的方式采用了监视器的概念;Java的每一个对象都有一个内部锁和内部条件;但是他在以下三个方面的不同于监视器使其安全性下降:
a:域不要求必须是private
b:方法要求必须是synchronized;
c:内部锁对客户端是可以使用的 -
F:Volaitile域
目前现状是:
a:多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值,这将使得运行在不同处理器上的线程可能在同一内存位置上取得不同的值;
b:编译器可以改变指令的运行顺序使得指令的吞吐量最大化;虽然这种变化不会改变代码的语义但是,编译器假定内存中的值仅仅只是在代码有显示修改指令时才会改变;然而内存的值是可以被其他线程改变的;
有时仅仅为了的读写一两个域就使用同步显然会使得程序开销过大;此时我们就提出了Volatile概念;通过对一个变量(域)添加Volatile关键字来保证变量(域)安全相对同步来讲要容易的多;例如:
private synchronized boolean isDone;
private synchronized void setDone;
private boolean done;
//这种情况下我们就可以将done域声明为Volatile类型的;
private boolean isDone;
private void setDone;
private volatile boolean done;
线程写volatile变量的过程
①改变线程工作内存中volatile变量副本的值
②将改变后的副本的值从工作内存刷新到主内存
线程读volatile变量的过程:
①从主内存中读取volatile变量的最新值到线程的工作内存中
②从工作内存中读取volatile变量的副本的值
虽然volatile关键字给我们的省去了同步带来的不便利性,但是同时它也存在一定的安全隐患:
a:volatile是不能保证原子性的例如:done!=done;a++等操作
b:volatile禁止指令重新排序;
总之:以下3种情况下,域的并发访问时安全的:
a:于是final,并且在构造器调用完成之后被访问;
b:对域的访问使用共有锁进行保护
c:域是volatile的