java 多线程技术
java 多线程不愧是java语言的核心部分和难点部分,想把他学好,理解同其实真的是挺不容易的,在经过几天的看书之后,现在打算来总结一下吧。
创建多线程的方式:
- 1 使用继承的方式:
dome 如下:
public class MyThread extends Thread {
public static void main ( String args[] ) {
Thread a = new MyThread(); //(1) 创建线程实例
a.start(); //(2) 启动线程
System.out.println("This is main thread."); //(3)
}
public void run() {
System.out.println("This is another thread."); //(4)
}
}
启动线程使用.start 方法.run方法不产生线程哦。
- 2 利用接口产生线程:
public class MyThread implements Runnable {
public static void main ( String args[] ) {
MyThread my = new MyThread(); //(1)
Thread a = new Thread(my); //(2) 创建线程实例,实现Runnable接口的类对象
作为实参
a.start(); //(3)
System.out.println("This is main thread."); //(4)
}
public void run() {
System.out.println("This is another thread.");//(5)
}
}
线程对象去调用Runnable 接口实现的对象。
- 3 使用Callable 接口,该接口object 可以为任意包装类,代表你所要返回的数据类型。
Dome 如下:
import java.util.ArrayList;
import java.util.concurrent.*;
public class CallableDome {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService exce = Executors.newCachedThreadPool();
ArrayList<Future<String>> result = new ArrayList<Future<String>>();
for (int i = 0; i < 10; i++) {
result.add(exce.submit(new TaskWithResult(i)));
}
for (Future<String> string : result) {
System.out.println(string.get());
}
}
}
import java.util.concurrent.Callable;
public class TaskWithResult implements Callable<String > {
int id;
TaskWithResult(int id)
{this.id=id;}
@Override
public String call() throws Exception {
return "hello "+id;
}
}
总结:1 所代表创建线程的方式,最为单调,最普通的逻辑,无返回至,不需要共享资源的使用1,第二种方式可用于,需要多个线程处理一个对象且无返回值,而第三个的话有返回值,有资源处理,且一般搭配线程池来使用。
多个线程对同一对象进行处理时的资源共享的问题:
首先看一段代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestThread{
public static void main(String[] args){
Resource m = new Resource(1000);
Thread t1 = new Thread(m);
Thread t2 = new Thread(m);
t1.start();
t2.start();
}
}
class Resource implements Runnable {
private Lock mylock=new ReentrantLock();
public int i;
public Resource(int _i){
i = _i;
}
public void run(){
while (true) {
if (i > 0) {
i--;
System.out.println(Thread.
currentThread().getName() + " " + i);
} else break;
}
}
}
这段代码会导致一个问题:
会出现输出的数字相同的情况。这里出现问题的原因就是因为资源共享的问题。
例如:i-- 这个步骤不具备原子性,可分为3部分,
1 虚拟机从内存中取值,放入寄存器中。
2 对寄存器中i进行–操作。
3 将值返回到内存中。
如果某一线程进入2过程时,有其他线程抢占了资源,并完成3这就会导致了,原先的线程对i的操作冲突,导致出现相同的结果。
== 解决方法:==
- 使用Lock 对象,生成一个锁对象,对数据进行管理。
方法如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestThread{
public static void main(String[] args){
Resource m = new Resource(1000);
Thread t1 = new Thread(m);
Thread t2 = new Thread(m);
t1.start();
t2.start();
}
}
class Resource implements Runnable {
private Lock mylock=new ReentrantLock();
public int i;
public Resource(int _i){
i = _i;
}
public void run(){
while (true) {
mylock.lock();
try
{if (i > 0) {
i--;
System.out.println(Thread.
currentThread().getName() + " " + i);
} else break;}finally {
mylock.unlock();
}
}
}
}
将你需要保持原子性的代码给包括在内,记得必须要使用的格式:
mylock.lock();
try{ 代码块}
finally{mylock();}
因为你所写的代码块中,可能会出现异常,导致线程被阻塞,不能进入下一步,导致死锁的问题,使用finally之后,哪怕出现异常也能将锁打开,从而使其他线程继续进行,而不是产生死锁。
与Lock对象有关的一个对象,条件对象:
private Lock mylock=new ReentrantLock();
private Condition myCondition=mylock.newCondition();
该对象的方法:void await();
将该线程放入条件等待集之中。
void signalAll()
解除等待集中所有线程的阻塞状态。
void signal ( )
从该条件的等待集中随机地选择一个线程, 解除其阻塞状态。
- 使用synchronized关键字。
这可将一段代码,在运行的时候置为唯一运行的,当某一线程运行时,其他线程就被阻塞了。也可用在方法上,将方法都置为是唯一运行的
用法如下:
class Resource implements Runnable {
volatile public int i;
volatile public Integer it;
public Resource(int _i){
i = _i;
it = new Integer(i);
}
public void run(){
while(true){
synchronized(it){
if (i>0){
try{
Thread.sleep(200);
}
catch(Exception e){}
i--;
System.out.println(Thread.
currentThread().getName()+" "+i);
}
else{
System.out.println(Thread.
currentThread().getName());
break; }}
}}}
public class TestSecurity{
public static void main(String[] args){
Resource m = new Resource(9);
Thread t1 = new Thread(m);
Thread t2 = new Thread(m);
t1.start();
t2.start();
}
}
同步块又称为临界区,保证同一时间只有一个线程执行同步块内的代码。
- volatile 关键字的使用。
volatile 关键字的使用,主要是用于一些不涉及复杂操作的变量,加入某个变量,被使用,volatile 关键字,标识之后,他所进行的一些简单语句,如:赋值,++,–等,就不会出现原子性的问题,如i–;
1 虚拟机从内存中取值,放入寄存器中。
2 对寄存器中i进行–操作。
3 将值返回到内存中。
这三个步骤将被一个线程,一次性执行,中间不会被某些其他的线程阻塞导致结果出错。
想要对一些线程的变量进行运算时,为保持其操作的原子性,Java中还提供了一些方法,帮助实现。
例如:
public static AtomicLong nextNumber = new AtomicLongO ;
// In some thread...
long id = nextNumber.increinentAndGetO:
使用该对象可对其-1,且不受线程的干扰。
如果要进行较为复杂的运算的话,使用AtomicLong 显然是不够的,这个事后就需要,一种更为通用的方法,例如使用compareAndSet 方法。
这个更新不是原子的。实际上,应当在一个循环中计算新值和使用 compareAndSet:
do {
oldValue = largest.getO;
newValue = Math ,max (oldValue , observed);
} while (!largest.compareAndSet(oldValue, newValue));
这里简单来说有一点坑,这个函数不太好想他的作用,
他的作用大概就是这样,因为会先获取到largest里面所得到的值,然后又获得需要更新的值,传给了compareAndSet 对象,如果说这个时候,又其他线程成功的将compareAndSet方法执行完,这个时候
compareAndSet 内部的值已经不和传进去的oldvalues的值相等,这个时候返回false,这个时候继续执行循最终, 它会成功地用新值替换原来的值。这听上去有些麻烦, 不过 compareAndSet 方
法会映射到一个处理器操作, 比使用锁速度更快。
但也有一个缺点,加入非常多的线程都同时去执行compareAndSet的代码块,然后就会导致大量竟争,导致性能大幅度下降,这个时候就需要其他的对象LongAdder
LongAccumulator adder = new LongAccumulator(Long::sum, 0);
// In some thread...
adder.accumulate(value);
死锁问题
不当的程序设计逻辑都可能会导致死锁的发生,你只能更改逻辑,使其尽量不会发生死锁现象,Java本身是没有对发生死锁时进行规避的机制,不能滥用锁,需考虑好锁的使用。
线程局部变量问题:
假设你的某个类需要一个产生随机数的成员对象,但是该类实例化一个对象之后,你会使用多个线程去调用这个对象,当用到产生随机数的成员函数的时候,就会很低效,这个时候你就需要一个线程局部变量,专属于你所创建的多线程
代码如下:
public static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
//要访问具体的格式化方法,可以调用:
String dateStamp = dateFormat.get().format(new DateO);
在一个给定线程中首次调用 get 时, 会调用 initialValue 方法。在此之后, get 方法会返回
属于当前线程的那个实例。