关于并发中多线程的问题

为了提高cpu的利用率,我们采用多线程来提高效率。

1.什么是进程?什么是线程?

进程简单的来说就是一个程序的执行,而线程可以理解为在进程中独立运行的多个子任务。

2.创建线程的方式

创建线程的方式有多种,可以继承thread类,实现runnable接口,实现里面的run方法。当然有时为了避免直接创建Thread对象,我们也可以使用executor类来管理thread对象,从而简化并发编程。下面我们来详细介绍这三种方式创建线程的实例:

(1).继承Thread类

运行的结果如下图,该例子同时展现了线程的随机性

(2)实现runnable接口

主方法中只要写下面一句代码,即可运行

(3)使用Executor来创建线程的方法

ExecutorService exec=Executors.newCachedThreadPool()

CachedThreadPool在程序执行的过程中通常会创建与所需数量相同的线程,然后再它回收旧线程时停止创建新线程。因此它是合理的Executor的首选

ExecutorService exec=Executors.newFixedThreadPool(5)

FixedThreadPool可以一次性预见先执行代价高昂的线程分配,这可以节省时间,你不用为每个任务都固定的付出创建线程的开销,相当于开辟了一个数量有限的线程池。

ExecutorService exec=Executors.newSingleThreadPool()

SingleThreadPool相当于数量为1的FixedThreadPool。

这就是创建线程的三种方式。

3同步synchronized,Lock,volatile关键字,ThreadLocal的使用

非线程安全的问题存在在实例变量中,也就是类的私有属性中,如果方法内部有私有变量,则不存在“非线程安全”的问题。解决这种问题,我们可以只用同步方法或者同步代码块。调用synchronized声明的方法一定是排队运行的。另外牢记“共享”这两个字,只有共享资源的读写访问才需要同步化,如果不是共享的资源,那么根本没有同步的必要。

下面看一个例子:

package service;
public class HasSelfPrivateNum {
private int num=0;
synchronized public void addI(String username){
try{
if(username.equals("a")){
num=100;
System.out.println("a set over!");
Thread.sleep(2000);
}else{
num=200;
System.out.println("b set over");
}
System.out.println(username+" num="+num);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}

package service;
public class ThreadA extends Thread{
private HasSelfPrivateNum privateNum;
public ThreadA(HasSelfPrivateNum privateNum){
super();
this.privateNum=privateNum;
}
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
privateNum.addI("a");
}

}

package service;
public class ThreadB extends Thread{
private HasSelfPrivateNum privateNum;
public ThreadB(HasSelfPrivateNum privateNum){
super();
this.privateNum=privateNum;
}
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
privateNum.addI("b");
}

}

package service;
public class Run {
public static void main(String[] args) {
HasSelfPrivateNum privateNum1 = new HasSelfPrivateNum();
HasSelfPrivateNum privateNum2 = new HasSelfPrivateNum();
ThreadA threadA = new ThreadA(privateNum1);
threadA.start();
ThreadB threadB = new ThreadB(privateNum2);
threadB.start();
}

}

运行的结果如下图所示:

这就是多个对象多个锁的情况,实例了两个对象,线程执行同步方法是需要锁对象,但并不是同一个锁,所以b不需要等待a释放锁之后再执行。多个线程访问多个对象的时候,JVM就会创建多个锁。

synchronized锁重入

“可重入锁”的概念是:自己可以再次获取自己的内部锁,比如有一条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果锁不重入,就会造成死锁。当存在父子继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法。

当一个线程出现异常时,其所持有的锁会自动释放。同步不具有继承性。

用关键字synchronized声明的方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程则必须等待较长的时间。在这种情况下可以使用synchronized同步语句块来解决。synchronized方法是对当前对象加锁,而synchronized代码块是对某一个对象加锁。当两个并发线程访问同一个对象object中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

在使用同步synchronized(this)代码块的时候需要注意的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器”只有一个。

关键字synchronized还可以应用在static静态方法上,如果这样写,那是对当前的*.java文件对应的Class类进行持锁。而synchronized关键字加到非static静态方法上是给对象上锁。同步静态代码块,给class上锁后,不同的对象去调用方法都是同步的。

数据类型string的常量池特性,在JVM中具有String常量池缓存的功能。

package service;
public class Test {
public static void main(String[] args) {
String a= "a";
String b= "a";
System.out.println(a==b);
}
}

上面的运行结果为true,这就是String常量池的特点。看看下面的例子:

package Test;
public class Service {
public static void print(String stringParam){
synchronized(stringParam){
while(true){
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

}

package Test;
public class ThreadA extends Thread{
private Service service;
public ThreadA(Service service){
super();
this.service=service;
}
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
service.print("AA");
}
}

package Test;
public class ThreadB extends Thread{
private Service service;
public ThreadB(Service service){
super();
this.service=service;
}
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
service.print("AA");
}

}

 

package Test;
public class Run {
public static void main(String[] args) {
Service service = new Service();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
}

}

运行 的结果都是AA,这是因为两个线程持有相同的锁,所以造成线程B不能执行,这就是String常量池带来的后果,一般用new Object()来实例化一个对象。

多线程中的死锁的问题

java线程死锁的问题是一个经典的多线程的问题,因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。造成线程假死的状态。下面是一个死锁的例子:

package deadLockTest;
public class DeadThead implements Runnable {
public String username;
public Object lock1=new Object();
public Object lock2=new Object();
public void setFlag(String username){
this.username=username;
}
@Override
public void run() {
// TODO Auto-generated method stub
if(username.equals("a")){
synchronized(lock1){
try {
System.out.println("username="+username);
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch blocka
e.printStackTrace();
}
synchronized(lock2){
System.out.println("按lock1->lock2代码的顺序执行了");
}
}
}
if(username.equals("b")){
synchronized(lock2){
try {
System.out.println("username="+username);
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(lock1){
System.out.println("按lock2->lock1代码的顺序执行了");
}
}
}
}
}

package deadLockTest;
public class Run {
public static void main(String[] args) {
try {
DeadThead deadThead1 = new DeadThead();
deadThead1.setFlag("a");
Thread thread1 = new Thread(deadThead1);
thread1.start();
Thread.sleep(100);
deadThead1.setFlag("b");
Thread thread2 = new Thread(deadThead1);
thread2.start();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}

可以使用JDK自带的工具来监测是否有死锁现象,首先进入CMD工具,再进入JDK的安装文件bin目录下,执行jsp命令,在得到运行的线程Run的id值是3244,在执行jstack命令,查了那结果。

volatile   私有堆栈的值和公共堆栈的值不同步的时候可以使用volatile.它的作用是强制从公共堆栈中取值。

下面比较关键字synchronized和volatile;

(1)关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能用来修饰变量,而synchronized可以修饰方法,以及代码块。

(2)多线程访问volatile的时候不会发生阻塞。而synchronized会出现阻塞。

(3)volatile能保证数据的可见性,但不能保证数据的原子性,而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。

线程安全包含原子性和可见性两方面,java的同步机制就都是围绕这两个机制来实现线程安全的。

修改实例变量中的数据i++不具有原子性

表达式i++的步骤解析:

(1)从内存中取出i的值

(2)计算i的值

(3)将i的值写入内存中

假如在第二步计算值的时候,另外一个线程也修改i的值,那么这个时候就会出现脏数据,解决的办法其实就是使用synchronized关键字.

4.线程间的通信

等待和唤醒机制

wait()的作用是使当前线程处于预等待状态,wait()方法是Object类的方法,wait()只在同步方法和同步代码块中使用,因为当前对象持有锁,当调用wait方法后,线程会释放锁,若没有锁,程序就会抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此不需要try catch进行捕捉。

notify()也需要在同步代码块和同步方法中使用,因为在调用之前,线程也需要获得该对象的对象级别锁。如果有多个线程等待,则由线程规划器随机挑选出其中一个呈wait状态的线程。在执行notify()后,当前线程不会马上该对象的锁,呈wait()状态的线程并不能马上获得该线程对象的锁。要等到执行notify()方法的线程将程序执行完,也就是退出synchronized代码块之后。

下面举例说明生产者和消费者模式的实现

package entity;
public class ValueObject {
public static String value="";

}

package entity;
public class C {
private String lock;
public C(String lock){
this.lock=lock;
}
public void getValue(){
synchronized (lock) {
try {
if(ValueObject.value.equals("")){
lock.wait();
}
System.out.println("get的值是:"+ValueObject.value);
ValueObject.value="";
lock.notify();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}

 

package entity;
public class P {
private String lock;
public P(String lock){
this.lock=lock;
}
public void setValue(){
synchronized (lock) {
try {
if(!ValueObject.value.equals("")){
lock.wait();
}
String value=System.currentTimeMillis()+"_"+System.nanoTime();
System.out.println("set的值是:"+value);
ValueObject.value=value;
lock.notify();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}

package entity;
public class ThreadC extends Thread {
private C c;
public ThreadC( C c){
super();
this.c=c;
}
@Override
public void run() {
while(true){
c.getValue();
}
}

}

package entity;
public class ThreadP extends Thread {
private P p;
public ThreadP(P p){
super();
this.p=p;
}
@Override
public void run() {
while(true){
p.setValue();
}
}

}

 

package entity;
public class Run {
public static void main(String[] args) {
String lock = new String("");
P p =new P(lock);
C c=new C(lock);
ThreadP pthread = new ThreadP(p);
ThreadC cthread = new ThreadC(c);
pthread.start();
cthread.start();
}

}

部分的打印结果为

这个例子是一个生产者和一个消费者的情况,但实际可能是对个生产者和多个消费者,这时我们采用notifyAll()的方法就可以了。

5通过管道进行线程间的通信:字节流和字符流

管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输入管道中读取数据。

java JDK中提供了4个类来是线程间可以进行通信:

(1)PipedInputStream和PipedOutputStream

(2)PipedReader和PipedWriter

给出下面的例子;

package pipedStream;
import java.io.IOException;
import java.io.PipedOutputStream;
public class WriteData {
public void writeMethod(PipedOutputStream out){
try{
System.out.println("write : ");
for (int i = 0; i <300; i++) {
String outData=""+(i+1);
out.write(outData.getBytes());
System.out.print(outData);
}
System.out.println();
out.close();
}catch(IOException e){
e.printStackTrace();
}
}

}

 

package pipedStream;
import java.io.IOException;
import java.io.PipedInputStream;
public class ReadData {
public void readMethod(PipedInputStream input){
try{
System.out.println("read : ");
byte[] byteArray=new byte[20];
int readLength = input.read(byteArray);
while(readLength!=-1){
String newData=new String(byteArray,0,readLength);
System.out.print(newData);
readLength = input.read(byteArray);
}
System.out.println();
input.close();
}catch(IOException e){
e.printStackTrace();
}
}

}

 

package pipedStream;


import java.io.PipedInputStream;
public class ThreadRead extends Thread{
private ReadData read;
private PipedInputStream input;

public ThreadRead(ReadData read,PipedInputStream input){
super();
this.read=read;
this.input=input;
}
public void run() {
// TODO Auto-generated method stub
read.readMethod(input);
}

}

 

package pipedStream;
import java.io.PipedOutputStream;
public class ThreadWrite extends Thread{
private WriteData write;
private PipedOutputStream out;

public ThreadWrite(WriteData write,PipedOutputStream out){
super();
this.write=write;
this.out=out;
}
public void run() {
// TODO Auto-generated method stub
write.writeMethod(out);
}

}

package pipedStream;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
public class Run {
public static void main(String[] args) {
try {
WriteData write = new WriteData();
ReadData read = new ReadData();
PipedOutputStream out = new PipedOutputStream();
PipedInputStream input = new PipedInputStream();
out.connect(input);

ThreadRead threadRead = new ThreadRead(read, input);
threadRead.start();
Thread.sleep(2000);

ThreadWrite threadWrite = new ThreadWrite(write, out);
threadWrite.start();
} catch (Exception e) {
e.printStackTrace();
}
}

}

运行的结果:

6.join()方法的使用

方法join的作用是使所属的线程对象X正常执行run()方法中的任务,而使当前线程Z进行无限期的阻塞,等待线程X销毁后在继续执行线程Z后面的代码。方法join具有使线程排队运行的作用,有些类似同步的运行效果。join与synchronized的区别是;join在内部使用wait()方法进行等待,而synchronized是使用的对象监视器原理作为同步。

方法join(long)的功能是在内部使用wait(long)方法实现,所有join(long)的方法具有释放锁的特点,而Thread.sleep(long)方法不释放锁。

7.类ThreadLocal的使用

类ThreadLocal主要解决的就是每个线程绑定自己的值,可以将ThreadLocal类比喻成全局存放数据的盒子,盒子中可以存储每个线程的私有数据。

下面是验证线程隔离性的例子

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值