95992828九五至尊2

Java二十四线程编制程序中ThreadLocal类的用法及一语破的,类别号管理的主意九五至尊1老品牌值得

三月 9th, 2019  |  九五至尊1老品牌值得

三个表存放系列号记录,每条记下包蕴初步连串号和终结号

Java三十二线程编程中ThreadLocal类的用法及一语破的,javathreadlocal用法

ThreadLocal,直译为“线程本地”或“本地线程”,假设你实在如此觉得,那就错了!其实,它便是二个器皿,用于存放线程的局地变量,小编觉着应当称为
ThreadLocalVariable(线程局部变量)才对,真不明白为啥当初 Sun
公司的工程师那样命名。

早在 JDK 1.2 的时代,java.lang.ThreadLocal
就诞生了,它是为着缓解多线程并发难题而布署的,只不过设计得有点难用,所以于今从没赢得大规模运用。其实它照旧挺有用的,不信任的话,大家共同来看望那么些例子吗。

贰个行列号生成器的次第,大概还要会有多个线程并发访问它,要保管各样线程获得的行列号都以自增的,而无法相互干扰。

先定义多个接口:

public interface Sequence {

  int getNumber();
}

每趟调用 getNumber() 方法可取得3个队列号,下次再调用时,体系号会自增。

再做1个线程类:

public class ClientThread extends Thread {

  private Sequence sequence;

  public ClientThread(Sequence sequence) {
    this.sequence = sequence;
  }

  @Override
  public void run() {
    for (int i = 0; i < 3; i++) {
      System.out.println(Thread.currentThread().getName() + " => " + sequence.getNumber());
    }
  }



}

在线程中连连输出1回线程名与其相应的体系号。

我们先不用 ThreadLocal,来做3个贯彻类吧。

public class SequenceA implements Sequence {

  private static int number = 0;

  public int getNumber() {
    number = number + 1;
    return number;
  }

  public static void main(String[] args) {
    Sequence sequence = new SequenceA();

    ClientThread thread1 = new ClientThread(sequence);
    ClientThread thread2 = new ClientThread(sequence);
    ClientThread thread3 = new ClientThread(sequence);

    thread1.start();
    thread2.start();
    thread3.start();
  }
}

队列号初叶值是0,在 main() 方法中模仿了四个线程,运转后结果如下:

Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
Thread-2 => 4
Thread-2 => 5
Thread-2 => 6
Thread-1 => 7
Thread-1 => 8
Thread-1 => 9

由于线程运维顺序是即兴的,所以并不是0、一 、2这么的各类,这些好精晓。为何当
Thread-0 输出了① 、② 、3自此,而 Thread-2
却输出了④ 、⑤ 、6吗?线程之间照旧共享了 static
变量!这正是所谓的“非线程安全”难题了。

那便是说什么样来担保“线程安全”呢?对应于那一个案例,正是说分歧的线程可具有本身的
static 变量,怎么样促成啊?上面看看此外3个落到实处吗。

public class SequenceB implements Sequence {

  private static ThreadLocal<Integer> numberContainer = new ThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
      return 0;
    }
  };

  public int getNumber() {
    numberContainer.set(numberContainer.get() + 1);
    return numberContainer.get();
  }

  public static void main(String[] args) {
    Sequence sequence = new SequenceB();

    ClientThread thread1 = new ClientThread(sequence);
    ClientThread thread2 = new ClientThread(sequence);
    ClientThread thread3 = new ClientThread(sequence);

    thread1.start();
    thread2.start();
    thread3.start();
  }
}

通过 ThreadLocal 封装了一个 Integer 类型的 numberContainer
静态成员变量,并且开始值是0。再看 getNumber() 方法,首先从
numberContainer 中 get 出近来的值,加1,随后 set 到 numberContainer
中,最终将 numberContainer 中 get 出近日的值并再次来到。

是否很恶心?但是很强大!确实稍微饶了瞬间,大家不妨把 ThreadLocal
看成是2个容器,这样精通就大约了。所以,那里故意用 Container
那么些单词作者为后缀来定名 ThreadLocal 变量。

运营结果什么呢?看看啊。

Thread-0 => 1
Thread-0 => 2
Thread-0 => 3
Thread-2 => 1
Thread-2 => 2
Thread-2 => 3
Thread-1 => 1
Thread-1 => 2
Thread-1 => 3

各种线程相互独立了,同样是 static
变量,对于不一致的线程而言,它没有被共享,而是各样线程各一份,那样也就保障了线程安全。
也便是说,TheadLocal 为每1个线程提供了叁个独立的副本!

搞了解 ThreadLocal 的规律之后,有供给总括一下 ThreadLocal 的
API,其实很简短。

  • public void set(T value):将值放入线程局部变量中
  • public T get():从线程局地变量中获取值
  • public void remove():从线程局地变量中移除值(有助于 JVM 垃圾回收)
  • protected T initialValue():再次来到线程局地变量中的初叶值(默许为
    null)

为啥 initialValue() 方法是 protected
的呢?正是为着唤起程序员们,那几个艺术是要你们来达成的,请给那个线程局部变量1个开始值吧。

刺探了规律与这么些 API,其实想想 ThreadLocal 里面不便是包裹了三个 Map
吗?本人都足以写一个 ThreadLocal 了,尝试一下吗。

public class MyThreadLocal<T> {

  private Map<Thread, T> container = Collections.synchronizedMap(new HashMap<Thread, T>());

  public void set(T value) {
    container.put(Thread.currentThread(), value);
  }

  public T get() {
    Thread thread = Thread.currentThread();
    T value = container.get(thread);
    if (value == null && !container.containsKey(thread)) {
      value = initialValue();
      container.put(thread, value);
    }
    return value;
  }

  public void remove() {
    container.remove(Thread.currentThread());
  }

  protected T initialValue() {
    return null;
  }
}

如上完全山寨了1个 ThreadLocal,个中中定义了一个联手
Map(为什么要如此?请读者自行思考),代码应该卓殊不难读懂。
下边用那 MyThreadLocal 再来完结一把看看。

public class SequenceC implements Sequence {

  private static MyThreadLocal<Integer> numberContainer = new MyThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
      return 0;
    }
  };

  public int getNumber() {
    numberContainer.set(numberContainer.get() + 1);
    return numberContainer.get();
  }

  public static void main(String[] args) {
    Sequence sequence = new SequenceC();

    ClientThread thread1 = new ClientThread(sequence);
    ClientThread thread2 = new ClientThread(sequence);
    ClientThread thread3 = new ClientThread(sequence);

    thread1.start();
    thread2.start();
    thread3.start();
  }
}

 以上代码其实就是将 ThreadLocal 替换到了
MyThreadLocal,仅此而已,运维效果和前边的一律,也是不错的。

实际 ThreadLocal 能够独立成为一种设计形式,就看您怎么看了。

ThreadLocal 具体有何使用案例呢?

自身想首先要说的正是:通过 ThreadLocal 存放 JDBC
Connection,以实现工作控制的能力。

恐怕保持本人定位的 Style,用1个 德姆o
来说话呢。用户建议一个要求:当修改产品价格的时候,需求记录操作日志,什么日期做了什么工作。

大概这些案例,只若是做过使用种类的同伙们,都应该境遇过啊?无外乎数据Curry就两张表:product
与 log,用两条 SQL 语句应该能够缓解难点:

update product set price = ? where id = ?
insert into log (created, description) values (?, ?)

But!要确认保障那两条 SQL 语句必须在同八个事情里开始展览提交,否则有恐怕 update
提交了,但 insert
却尚无交给。若是那样的工作实在发生了,大家自然会被用户指着鼻子狂骂:“为啥产品价格改了,却看不到几时改的吗?”。

聪明的自家在收到那些需要今后,是这般做的:

第贰,笔者写多个 DBUtil 的工具类,封装了数据库的常用操作:

public class DBUtil {
  // 数据库配置
  private static final String driver = "com.mysql.jdbc.Driver";
  private static final String url = "jdbc:mysql://localhost:3306/demo";
  private static final String username = "root";
  private static final String password = "root";

  // 定义一个数据库连接
  private static Connection conn = null;

  // 获取连接
  public static Connection getConnection() {
    try {
      Class.forName(driver);
      conn = DriverManager.getConnection(url, username, password);
    } catch (Exception e) {
      e.printStackTrace();
    }
    return conn;
  }

  // 关闭连接
  public static void closeConnection() {
    try {
      if (conn != null) {
        conn.close();
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

其间搞了二个 static 的 Connection,那下子数据库连接就好操作了,牛逼吧!

然后,作者定义了二个接口,用于给逻辑层来调用:

public interface ProductService {

  void updateProductPrice(long productId, int price);
}

听他们说用户提议的急需,笔者想那一个接口完全够用了。依照 productId 去革新对应
Product 的 price,然后再插入一条数据到 log 表中。

骨子里业务逻辑也不太复杂,于是笔者不慢地做到了 ProductService 接口的落实类:

public class ProductServiceImpl implements ProductService {

  private static final String UPDATE_PRODUCT_SQL = "update product set price = ? where id = ?";
  private static final String INSERT_LOG_SQL = "insert into log (created, description) values (?, ?)";

  public void updateProductPrice(long productId, int price) {
    try {
      // 获取连接
      Connection conn = DBUtil.getConnection();
      conn.setAutoCommit(false); // 关闭自动提交事务(开启事务)

      // 执行操作
      updateProduct(conn, UPDATE_PRODUCT_SQL, productId, price); // 更新产品
      insertLog(conn, INSERT_LOG_SQL, "Create product."); // 插入日志

      // 提交事务
      conn.commit();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      // 关闭连接
      DBUtil.closeConnection();
    }
  }

  private void updateProduct(Connection conn, String updateProductSQL, long productId, int productPrice) throws Exception {
    PreparedStatement pstmt = conn.prepareStatement(updateProductSQL);
    pstmt.setInt(1, productPrice);
    pstmt.setLong(2, productId);
    int rows = pstmt.executeUpdate();
    if (rows != 0) {
      System.out.println("Update product success!");
    }
  }

  private void insertLog(Connection conn, String insertLogSQL, String logDescription) throws Exception {
    PreparedStatement pstmt = conn.prepareStatement(insertLogSQL);
    pstmt.setString(1, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date()));
    pstmt.setString(2, logDescription);
    int rows = pstmt.executeUpdate();
    if (rows != 0) {
      System.out.println("Insert log success!");
    }
  }
}

代码的可读性还算不错啊?那里作者使用了 JDBC 的尖端天性 Transaction
了。暗自庆幸了一番自此,笔者想是或不是有必不可少写三个客户端,来测试一下进行结果是或不是自家想要的呢?
于是本人偷闲,直接在 ProductServiceImpl 中加进了一个 main() 方法:

public static void main(String[] args) {
  ProductService productService = new ProductServiceImpl();
  productService.updateProductPrice(1, 3000);
}

自个儿想让 productId 为 1 的制品的价格修改为
贰仟。于是自个儿把程序跑了3遍,控制台出口:

Update product success!
Insert log success!

九五至尊1老品牌值得,有道是是对了。作为一名正式的程序员,为了万无一失,笔者肯定要到数据Curry在看望。没错!product
表对应的笔录更新了,log 表也插入了一条记下。这样就可以将 ProductService接口交付给别人来调用了。

多少个小时过去了,QA 四妹起始骂笔者:“小编靠!我才模拟了 十二个请求,你那个接口怎么就挂了?说是数据库连接关闭了!”。

视听这么的喊叫声,让笔者一身发抖,立马中断了自家的小摄像,赶紧打开
IDE,找到了这几个 ProductServiceImpl 那么些完成类。好像没有 Bug
吧?但小编今日不敢给他别的回答,笔者实在有点怕他的。

自个儿忽然想起,她是用工具模拟的,也正是模拟多少个线程了!那本身要好也能够效仿啊,于是小编写了三个线程类:

public class ClientThread extends Thread {

  private ProductService productService;

  public ClientThread(ProductService productService) {
    this.productService = productService;
  }

  @Override
  public void run() {
    System.out.println(Thread.currentThread().getName());
    productService.updateProductPrice(1, 3000);
  }
}

自小编用这线程去调用 ProduceService的不二法门,看看是或不是有标题。此时,作者还要再修改一下 main() 方法:

// public static void main(String[] args) {
//   ProductService productService = new ProductServiceImpl();
//   productService.updateProductPrice(1, 3000);
// }

public static void main(String[] args) {
  for (int i = 0; i < 10; i++) {
    ProductService productService = new ProductServiceImpl();
    ClientThread thread = new ClientThread(productService);
    thread.start();
  }
}

自个儿也照猫画虎 10 个线程吧,小编就不信万分邪了!

运作结果的确让本身很晕、很晕:

Thread-1
Thread-3
Thread-5
Thread-7
Thread-9
Thread-0
Thread-2
Thread-4
Thread-6
Thread-8
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after connection closed.
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:411)
at com.mysql.jdbc.Util.getInstance(Util.java:386)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1015)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:989)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:975)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:920)
at com.mysql.jdbc.ConnectionImpl.throwConnectionClosedException(ConnectionImpl.java:1304)
at com.mysql.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:1296)
at com.mysql.jdbc.ConnectionImpl.commit(ConnectionImpl.java:1699)
at com.smart.sample.test.transaction.solution1.ProductServiceImpl.updateProductPrice(ProductServiceImpl.java:25)
at com.smart.sample.test.transaction.ClientThread.run(ClientThread.java:18)

自作者靠!竟然在四线程的条件下报错了,果然是数据库连接关闭了。怎么回事呢?我陷入了沉思中。于是自己Copy 了一把那句报错音讯,在百度、谷歌,还有 OSC
里都找了,解答实在是奇怪。

自家豁然想起,既然是跟 Connection 有提到,这笔者就将重庆大学精力放在检查
Connection 相关的代码上呢。是否 Connection 不应当是 static
的吧?我当下统一筹划成 static 的显假如为了让 DBUtil 的 static
方法访问起来特别方便,用 static 变量来存放 Connection
也抓实了品质啊。怎么搞呢?

于是自身看到了 OSC 上特别激烈的一篇小说《ThreadLocal
那一点事儿》,终于才让自己清楚了!原来要使种种线程都具备自身的连日,而不是共享同1个总是,不然线程1有也许会关闭线程2的总是,所以线程2就报错了。一定是如此!

自小编快速将 DBUtil 给重构了:

public class DBUtil {
  // 数据库配置
  private static final String driver = "com.mysql.jdbc.Driver";
  private static final String url = "jdbc:mysql://localhost:3306/demo";
  private static final String username = "root";
  private static final String password = "root";

  // 定义一个用于放置数据库连接的局部线程变量(使每个线程都拥有自己的连接)
  private static ThreadLocal<Connection> connContainer = new ThreadLocal<Connection>();

  // 获取连接
  public static Connection getConnection() {
    Connection conn = connContainer.get();
    try {
      if (conn == null) {
        Class.forName(driver);
        conn = DriverManager.getConnection(url, username, password);
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      connContainer.set(conn);
    }
    return conn;
  }

  // 关闭连接
  public static void closeConnection() {
    Connection conn = connContainer.get();
    try {
      if (conn != null) {
        conn.close();
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      connContainer.remove();
    }
  }
}

自家把 Connection 放到了 ThreadLocal
中,那样各样线程之间就切断了,不会互相苦恼了。

除此以外,在 getConnection() 方法中,首先从 ThreadLocal 中(也便是connContainer 中) 获取 Connection,假若没有,就由此 JDBC
来创立连接,最终再把成立好的连日放入那些 ThreadLocal 中。能够把
ThreadLocal 看做是七个器皿,一点不假。

同一,我也对 closeConnection() 方法做了重构,先从容器中拿走
Connection,获得了就 close 掉,最后从容器元帅其 remove
掉,以保全容器的清爽。

那下应该行了吧?小编再也运维 main() 方法:

Thread-0
Thread-2
Thread-4
Thread-6
Thread-8
Thread-1
Thread-3
Thread-5
Thread-7
Thread-9
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!
Update product success!
Insert log success!

终于是不留余地了

http://www.bkjia.com/Pythonjc/1139878.htmlwww.bkjia.comtruehttp://www.bkjia.com/Pythonjc/1139878.htmlTechArticleJava多线程编程中ThreadLocal类的用法及深入,javathreadlocal用法
ThreadLocal,直译为“线程本地”或“本地线程”,若是您确实如此觉得,那就错…

就算是单条的行列号记录,四个值相同
另1个表存放使用过的行列号,形式同上。

询问种类号时举行巡回,与已公布的富有的多个值的可比,假诺不在个中,表达有标题。

即便在已经用过的表里面也有标题

相关文章

Your Comments

近期评论

    功能


    网站地图xml地图