1、如何在java中创键线程
1、继承Thread类,重写run方法
2、实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
3、通过Callable和Future Task创键线程
4、通过线程池创键线程
通过实现Callable接口实现多线程
1、创键Callable接口的实现类,并实现Call方法
2、创键Callable实现类的实现,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的Call方法的返回值
3、使用FutureTask对象作为Thread对象的target创键并启动线程
4、调用FutureTask对象的get() 来获取子线程执行结束的返回值
public class ThreadDemo03 {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Callable<Object> oneCallable = new Tickets<Object>();
FutureTask<Object> oneTask = new FutureTask<Object>(oneCallable);
Thread t = new Thread(oneTask);
System.out.println(Thread.currentThread().getName());
t.start();
}
}
class Tickets<Object> implements Callable<Object>{
//重写call方法
@Override
public Object call() throws Exception {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName()+"-->我是通过实现Callable接口通过FutureTask包装器来实现的线程");
return null;
}
}
通过线程池来实现多线程
Executors 是concurrent 并发包下的一个工具类,可以提供方法创建一个线程池
public class MyClass {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(2);
Callable<Integer> callable = new People();
Callable<Integer> c2 = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int count = 0;
for (int i = 51; i <= 100; i++) {
count = count+i;
}
return count;
}
};
//Future 接口,获得线程返回的结果 Future Task 类是他的一个实现类
Future<Integer> s1 = service.submit(callable);
Future<Integer> s2 = service.submit(c2);
Integer integer = s1.get();
Integer integer1 = s2.get();
//这里打印结果为5050,结果正确,
有一种特殊情况,就是在子线程还没有执行完毕的时候,主线程就执行完毕,导致其它线程无法执行完毕,
而这里就没有这种情况,是因为Future 接口中的get方法,执行此方法时会一直等待子线程执行完毕,获得返回值,
此时的主线程处于一种waiting状态,线程状态下面会讲
System.out.println(integer + integer1);
}
}
问题:怎么统计异步运算结果
对于线程池
在java中有java.util.concurrent 包
concurrent中有接口 execute方法,
可以看出该方法是没有返回值的,
其次还有一个接口 ExecutorService 继承Executor
该接口中也有一一个方法可以实现异步运算,但是这个含有返回值
2、线程的几种状态
在java.lang包下有一个枚举类,Thread.State ,定义了线程的几种状态
阻塞状态就是:线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
等待状态:处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。比如使用join等方法时,
超时等待:处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。比如遇到sleep方法等,时间一过就自动唤醒
3、Java中Runnable和Callable接口的区别
- Runnable 接口实现run方法,Callable接口实现call方法
- 实现Callable接口的任务线程能返回执行结果,实现Runnable接口的任务线程不能返回执行结果
- Callable几口的call()方法允许抛出异常,而Runnable接口的run方法的异常只能内部消化
4、shutdown方法和isTerminated方法的使用
public class MyClassTwo {
static int i = 0;
public static void main(String[] args) throws Exception{
//创建线程池
ExecutorService es = Executors.newFixedThreadPool(2);
Runnable r1 = new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 100; i1++) {
i++;
}
}
};
es.submit(r1);
System.out.println("执行结果是==========》" + i);
}
}
上面的代码执行的结果是:0
原因是主线程率先完成了
解决方案,加shutdown和isTerminated方法
public class MyClassTwo {
static int i = 0;
public static void main(String[] args) throws Exception{
ExecutorService es = Executors.newFixedThreadPool(2);
Runnable r1 = new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 100; i1++) {
i++;
}
}
};
es.submit(r1);
//启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务
es.shutdown();
//isTerminated 如果所有任务在关闭后完成,则返回true
while(!es.isTerminated()){
}
System.out.println("执行结果是==========》" + i);
}
}
执行结果为:100
什么是ThreadLocal,它的实现原理是什么
多线程应用中,如果希望一个变量隔离在某个线程内,即:该变量只能由某个线程本身可见,其他线程无法访问,那么可以使用ThreadLocal
class TestClass implements Callable<String> {
@Override
public String call() throws Exception {
ThreadLocal<String> t1 = new ThreadLocal<>();
t1.set("天王盖地虎");
for (int i = 0; i < 10; i++) {
System.out.println("线程遍历了10次");
}
ThreadLocal<String> t2 = new ThreadLocal<>();
t2.set("宝塔镇河妖");
System.out.println(t1.get());
System.out.println(t2.get());
return null;
}
}
每一个线程的内部,都有一个hashmap集合,在这个map中的key值是ThreadLocal对象,value值是我们传入的值
什么是线程不安全的:
当多个线程并发的访问同一个临界资源时
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读/写完,其他线程才可使用。不会出现数据不一致或者数据污染。
… … … … … … … … … … … … … . … … … … … … … … …
对于ReentrantLock 和 ReentrantReadWriteLock 有什么区别
对于ReentrantLock ,只是给读或者写加上了锁,不过注意的是解锁应该放到finally中,依然是读写等关系相互互斥,比如20个线程,2个写,18个读,每个操作1s ,则总计耗时20s+;
对于读写锁,读读不互斥,读写互斥,写写互斥,总计耗时为 3s+;
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
Lock rl = lock.readLock();
Lock wl = lock.writeLock();
4.1什么是线程池,为什么要用线程池
线程池的作用就是限制系统中执行线程的作用
为什么使用:
1、减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
2、可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下
java里面线程池的顶级借口是Executor,但是严格意义上将Executor并不是一个线程池,而只是一个执行线程的工具,真正的线程池接口是ExecutorService
5、如何获取一个线程安全的集合
线程安全的集合:vector
Conllections.synchronizedList 还有其他类型的,如set 、map等相似集合
CopyOnWriteArrayList
static List<String> strings = Collections.synchronizedList(new ArrayList<String>());
对于这种方式获得的线程安全的集合,我们可以翻看源代码
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
可以发现,它在添加、修改、查询等方法的外面都添加了同步,其实和vector集合没有什么区别,性能较低
对于CopyOnWriteArrayList集合
static CopyOnWriteArrayList<String> strings = new CopyOnWriteArrayList<>();
查看源代码
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取原数组,复制到新数组中
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
//将新的数据加到新的数组中
newElements[len] = e;
//将新的数组赋值给集合对象的数组上
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
对于读方法
public E get(int index) {
return get(getArray(), index);
}
发现读方法没有上锁,读的只是原来的数组,不会和写操作互斥
最后发现,CopyOnWriteArrayList集合中读操作没有加锁,添加、修改等加上了锁,因此
读读不互斥、读写不互斥、写写互斥,效率更高
总结:CopyOnWriteArrayList
线程安全的ArrayList ,加强版读写分离
写有锁,读无锁,读写之间不阻塞,优于读写锁
写入时,先Copy一个容器副本,再添加新元素,最后替换引用
使用方式和ArrayList无异
同样还有CopyOnWriteArraySet
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
查看源代码
/**
* Creates an empty set.
*/
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
发现CopyOnWriteArraySet集合中使用的是CopyOnWriteArrayList集合,也就是说set集合的底层原理和list集合一样,
这是set集合的添加方法
public boolean add(E e) {
return al.addIfAbsent(e);
}
发现,在添加的时候,比list集合中多了一步判断,就是判断集合中是否含有重复数据,如果有,就不添加到数组中
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
底层是一个数组
private transient volatile Object[] array;
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}
6、ConcurrentHashMap
线程安全的map集合,最大可以支持16个线程并发操作
hashtable同时只能有一个线程执行
在java7和java8中,hashmap和ConcurrentHashMap 发生了很大的变化,对于7版本中,
Hashmap的的存储结构是哈希表,哈希表就是数组加链表
其实就如图中一样,一共分为16个断,map中添加数据首先计算hash值,判断在哪个断中,每个断都设置了锁,如果多个线程并发访问断并不相同,那么他们就不会相互影响,可以并发操作
7、ConcurrentLinkedQueue 先进先出
1、线程安全,可高效读写的队列,高并发下性能最好的队列
2、无锁,cas比较交换算法,修改的方法包含三个核心参数(V,E,N)
3、V:要更新的变量,E:预期值 N:新值
4、只有当V==E 时,V=N;否则表示已被更新过,否则取消当前操作
Java中提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型列子是ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列
8、在concurrent包下有一个包atomic,包中有一些类是支持原子操作的,比如AtomicInteger
其实他的内部使用的和上面队列中的安全操作一样
package com.qf.com.qf.day01;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author dongbo
* @date 2018/11/22 16:46
*/
public class MyClassSeven {
static ExecutorService es = Executors.newFixedThreadPool(2);
static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000000; i++) {
count.addAndGet(1);
}
}
};
es.submit(runnable);
es.submit(runnable);
es.shutdown();
while(!es.isTerminated()){
}
System.out.println(count);
}
}